Use ABCs for less-restrictive type hints.

February 14, 2023

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 list that contains a collection of ExpiringPolicy 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 writing if 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.

PythonABCs

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.

© 2024 Everyday Superpowers

LINKS
About | Articles | Resources

Free! Four simple steps to solid python projects.

Reduce bugs, expand capabilities, and increase your confidence by building on these four foundational items.

Get the Guide

Join the Everyday Superpowers community!

We're building a community to help all of us grow and meet other Pythonistas. Join us!

Join

Subscribe for email updates.