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 alist
that contains a collection ofExpiringPolicy
objects. 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 typing.Iterable
:
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:
- A
Container
is an object that lets you know if it contains an objectYou would check containment by writingif object in container
. . - An
Iterable
is an object that allows you to loop over its members. -
Sized
is an object that can tell you how many items it holds. - A
Collection
includes 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 Sized
, Container
, and Iterable
classes.
Our code from above can further be improved by changing the FunctionType
type hint to Callable
:
from typing import Iterable, Callable
...
def generate_reports(
reports_to_run: Iterable[ExpiringPolicy], report_generator: Callable
) -> list[ReportData]:
The 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. .
Additionally, the 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 Callable
documentation.
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.