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.
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