Type hints are a great tool to communicate your intent with your code.
When starting with type hints, many people will document functions similar to how I did with this function:
def generate_reports( reports_to_run: list[ExpiringPolicy], report_generator: FunctionType ) -> list[ReportData]: ...
If you’re not used to type hints, the code above defines a function,
generate_reports , that takes two arguments:
reports_to_run, which is a
listthat contains a collection of
ExpiringPolicyobjects. These are objects defined in another part of the code that represents policies that will expire in a certain number of days and
report_generator, which is a function that creates reports for the expiring policies.
The function returns a list of
ReportData objects. They contain report files and associated metadata for all the reports created.
This is a good approach for type hints, but it can be improved.
Being robust improves communication.
The Robustness Principle encourages you to design your software to be generous in what your functions accept but be strict with what you return.
Applying that to type hints, we want to communicate the range of inputs the function is designed to handle while clearly defining the expected result.
In the case above, this function explicitly expects a list of
ExpiringPolicy objects. While this doesn’t sound restrictive, it’s telling users of this function that the collection of expiring policies must be wrapped in a list and cannot be anything else, like a tuple, set, generator, or dictionary values.
However, the way the function is written, it doesn’t care that the policies are in a list. Instead, it iterates over the collection. That’s key. We need to communicate that this function needs something to iterate over.
We can communicate that by changing the
list type hint to
from typing import Iterable ... def generate_reports( reports_to_run: Iterable[ExpiringPolicy], report_generator: FunctionType ) -> list[ReportData]: ...
Now users of this function won’t have their editors complain if they don’t convert their collection of
ExpiringPolicy objects into a list.
So, why did I write the code as I did, using the list type hint? There were two main reasons:
- That’s what the data was in. Plain and simple. I documented what I was getting from another function that a teammate or I wrote.
- I was not aware of any alternatives.
What alternatives exist?
Python has a collection of objects that help us describe how objects behave. They are called Abstract Base Classes. They are not objects you create. Instead, they are the building blocks for how Python objects behave. I recommend reading the documentation to get a better understanding of Python objectsReading will be incredibly beneficial, but don’t expect to understand it in one go. Bookmark it. Reread some of it today. Read more next week. Let it seep in over time. .
There’s a useful table on that page. It describes what it takes for an object to behave a certain way. For example:
Containeris an object that lets you know if it contains an objectYou would check containment by writing
if object in container. .
Iterableis an object that allows you to loop over its members.
Sizedis an object that can tell you how many items it holds.
Collectionincludes the behavior of the three classes above to allow you to know if an item is in it, how many items are in it, and loop over its members.
This is where the building blocks come in. The Python maintainers have named a combination of many of these elements and put them into this table.
Below is a visual representation of how the ABCs relate to each other. The arrows signify the object at the start of the arrow has (or “inherits”) the features of the object the arrow is pointing to.
You can see that the
Collection object (just left of center) has the features of the
Container , and
Our code from above can further be improved by changing the
FunctionType type hint to
from typing import Iterable, Callable ... def generate_reports( reports_to_run: Iterable[ExpiringPolicy], report_generator: Callable ) -> list[ReportData]:
Callable type hint is always a better choice than the
FunctionType hint. A
FunctionType hint communicates the object must be a function, but in Python classes can behave like a functionYou can make a class act like a function by implementing the
__call__ method. .
Callable type hint allows you to communicate the types of the arguments the function should accept, as well as it’s return value.Learn more in the
It’s your turn
The next time you come across type hints in your code, look for restrictive hints like
list or missing type hints and consider using one of Python’s Abstract Base Classes to communicate the behavior your code needs.