Polymorphism with Type Safety in Python
Overview
Polymorphism in object oriented programming is an effective way to reduce code
duplication and isolate behaviors. Without enforcing a type system in Python,
however, attempting to implement polymorphism can lead to a headache of runtime
errors. By instead enforcing static typing, we can rely on our tooling to detect
errors ahead of time.
For our example, we’re going to create a new file polymorphism.py
and add the
following fuction, which will call a method on an input object and print it to
the console
def print_name(obj):
print(obj.get_name())
Runtime errors
Since static types are not enforced in Python by default, we can define the following class:
class SomeClass():
def foo(self):
return 'bar'
Now, we can create an instance of this class and pass it into our function
if __name__ == '__main__':
some_object = SomeClass()
print_name(some_object)
Our tooling (I’m using pyright as a language server) will not detect any issues
but running the resulting file will cause an AttributeError
$ python polymorphism.py
Traceback (most recent call last):
File "/polymorphism.py", line 11, in <module>
print_name(some_object)
File "/polymorphism.py", line 2, in print_name
print(obj.get_name())
AttributeError: 'SomeClass' object has no attribute 'get_name'
We can leverage some static typing to help us catch issues like this before we run our code by adding some static typing
Static Typing
Let’s first create a new base class at the top of our file which will have a method get_name
class BaseClass():
def __init__(self):
self.name = 'Base class'
def get_name(self) -> str:
return self.name
We already see some static typing here, get_name
is returning a string
Now, we can update the print_name
function to require an input parameter of type BaseClass
.
By requiring this type, we now can guarantee that obj will now have the get_name
method.
def print_name(obj: BaseClass):
print(obj.get_name())
If our language server is working properly, we should now see that our last line print_name(some_object)
is giving us an error message
Argument of type "SomeClass" cannot be assigned to parameter "obj" of type
"BaseClass" in function "print_name"
"SomeClass" is incompatible with "BaseClass" (PyrightreportGeneralTypeIssues)
Adding Inheritance
Now, in order to fix our error, we will edit SomeClass
inherit the default behavior from BaseClass
Because SomeClass
implements BaseClass
, our language server will no longer complain because the classes are now compatible
class SomeClass(BaseClass):
def foo(self):
return 'bar'
Running our file will print the value of get_name
we defined in BaseClass
$ python polymorphism.py
Base class
Changing Behavior Using Polymorphism
With this static typing in place, we can define different implementations of the BaseClass
which are all compatible with our print_name
function. Let’s edit SomeClass
again
class SomeClass(BaseClass):
def __init__(self):
self.name = "I'm a child class"
Let’s also add another invocation of print_name
to run on an instance of our generic class
if __name__ == '__main__':
some_object = SomeClass()
print_name(some_object)
some_base_object = BaseClass()
print_name(some_base_object)
Running our file once more, we can see we now have different results for each run of print_name
$ python polymorphism.py
I'm a child class
Base class
Recap
Python’s optional type system can be used to enforce good coding practice and help enable good object-oriented concepts, like polymorphism. Static typing helps detect potential bugs early in the development process. For more information on when and why to use polymorphism, you can review the following links
https://stackify.com/oop-concept-polymorphism/
https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/polymorphism
https://stackoverflow.com/questions/1031273/what-is-polymorphism-what-is-it-for-and-how-is-it-used