pythonclasspropertiesduck-typingcode-structure

Should I check the type of input to my class properties?


I am a fairly new developer in Python and was getting accustomed to the idea of "Duck Typing".

I have a class I used in another module to do particular things with it, for example, access it's name and version. This module obviously cannot handle arbitrary values and needs the name to be a string and version to be an integer.

Currently, I am enforcing the type of input to my class properties using isinstance():

class ExampleObject(object):
    def __init__(self):
        self._name = None
        self._version = None

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, name):
        if not isinstance(name, str) and name is not None:
            raise ValueError("Expected string, got {0}".format(type(name)))
        
        self._name = name

    @property
    def version(self):
        return self._name

    @version.setter
    def version(self, version_number):
        if not isinstance(version_number, int) and version_number is not None:
            raise ValueError("Expected int, got {0}".format(type(version_number)))

        self._version = version_number
        

If I go with the idea of Duck typing, I should not be enforcing a particular type to my properties. If I drop my type checking and some other developer starts using these properties for other data types, it can lead to the module I wrote breaking. Basically, the module expecting an integer and it gets something else. What's the correct approach to this?

I checked several related questions regarding this but none of them covers what would happen if we skip the type checking and how to enforce that other developers don't break things because they were not aware of another module using these properties to perform type specific things.


Solution

  • Python's duck typing puts the onus on you to enforce it. If the parameter will be passed by the user, it can be a good idea to perform casting and/or type validation. For example, you can try casting version_number to an int using the built-in int() method. If this fails (ie. version_number cannot be converted to a string), the int() method raises a ValueError for you, which you can check for as follows:

    @version.setter
    def version(self, version_number):
        try:
            version_number = int(version_number)
        except ValueError: #when casting to int fails, Python raises a ValueError
            # handle error here
        #otherwise, version_number is now a valid int, so continue
    

    If you're the only one that will be using your code, you can forgo this validation at your own risk. Regardless of which you choose, it's also a good idea to use type hints. Type hints don't do anything at runtime (so they won't perform any casting/validation for you), but can help assist your IDE in reminding you what type is expected for a parameter or return. Type hints for your function might look as follows:

    @version.setter
    def version(self, version_number: int) -> None:
    

    No type hint is required on the self parameter, as your IDE can tell self will be an ExampleObject. The type hint int on the parameter version_number indicates that this parameter should be an int. Finally, the return type hint of None indicates this function doesn't return anything.

    Source: https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html