5

For some context, I am making a Roguelike game in Python using the Doyren library (libtcod). I am more used to C++ where objects are strongly typed.

I'm writing several classes, such as a GameMap, GameObject, etc. Many of these classes contain methods which expect certain types, for example:

class GameMap:
    ...
    def add_object(self, game_object, x, y):
        ...

This method adds the GameObject game_object to the coordinate (x, y) on the map. There are clearly several ways this function can be misused:

  • A non-GameObject could be passed as game_object
  • A non-integer could be passed as x or y
  • A negative integer could be passed as x or y
  • An integer that is beyond the map's width could be passed as x
  • An integer that is beyond the map's height could be passed as y

My question is this: What is the Pythonic way to handle method misuse?

I see several possibilities:

Option 1: Lay out a series of assertions at the start of the method:

def add_object(self, game_object, x, y):
    assert(isinstance(game_object, GameObject)
    assert(type(x) == type(y) == int)
    assert(0 <= x < self.map_width and 0 <= y < self.map_height)
    ...

These assertions get fairly repetitive as I copy+paste them into many of my methods in GameMap, which is why I also provide option 2:

Option 2: Write assertions in their own functions, and call those when needed to prevent copy+pasting

def check_game_object(self, game_object):
    assert(isinstance(game_object, GameObject)

def check_coordinate(self, x, y):
    assert(type(x) == type(y) == int)
    assert(0 <= x < self.map_width and 0 <= y < self.map_height)

def add_object(self, game_object, x, y):
    check_game_object(game_object)
    check_coordinate(x, y)
    ...

Option 3: Lay out a series of custom exceptions at the start of the method:

def add_object(self, game_object, x, y):
    if not isinstance(game_object, GameObject):
        raise InvalidParameterException("game_object not a GameObject")
    elif not type(x) == type(y) == int:
        raise InvalidParameterException("(x, y) not integers")
    elif not (0 <= x < self.map_width and 0 <= y < map.self_height)
        raise InvalidMapCell("x, y do not represent a valid map cell)
    ...

Option 4: Return failure indicator, and handle the issue at a higher level

def add_object(self, game_object, x, y):
    if not isinstance(game_object, GameObject):
        return False
    elif not type(x) == type(y) == int:
        return False
    elif not (0 <= x < self.map_width and 0 <= y < map.self_height)
        return False
    ...

Option X: Something else?

Any advice here would be greatly appreciated! I want to make sure I'm following a useful and maintainable pattern as I go on.

4
  • 2
    Assertions are not for verifying parameters. They are for ensuring that external non-user objects are as expected. Commented Oct 13, 2017 at 6:17
  • Thank you for your advice. Is there an alternative Pythonic way to enforce correct method use? Commented Oct 13, 2017 at 6:19
  • 3
    The usual way to do it in Python is to just use the object and catch any resulting exceptions. Commented Oct 13, 2017 at 6:21
  • 4
    Assertions are not for validating data, use ValueError (or a derived exception) for that. Assertions are for verifying the correctness of the program's logic, in other words, they're used to test for things which should never happen if the logic is correct. Thus if an assertion is raised it means you need to change that logic. Commented Oct 13, 2017 at 6:52

1 Answer 1

7

The assertion is to make sure that objects, results, return, etc are what we expect them to be. Though they can be used for variable's type checking, that's not what their real purpose is and it gets repetitive.

In your case, I would suggest to use python EAFP way of doing things. Let the operation be performed on function input and catch the exception if it's not what is expected. From Python glossary :

EAFP: Easier to ask for forgiveness than permission. This common Python coding style assumes the existence of valid keys or attributes and catches exceptions if the assumption proves false. This clean and fast style is characterized by the presence of many try and except statements. The technique contrasts with the LBYL(Look before you leap) style common to many other languages such as C.

A quick example:

def f(x):
    """If x is str a TypeError is raised"""
    return 1 + x

try:
    f('a')
except TypeError as e:
    # something here or raise a custom exception
    raise
Sign up to request clarification or add additional context in comments.

4 Comments

Thank you very much, this is what I was after.
Why a function for this and not try: _ = 1+"a"; instead?
@mazunki that's just a throwaway example
Sure, but making a throwaway function seems unnecessary, and overcomplicated/messy, while assigning to _ is equally, if not more, visually understandable.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.