Yet Another Way To Improve Python Code Readability

Another day, another article about code readability.
Whilst last time we talked about Python’s coding style guidelines, today I would like to tell you a bit about another feature of Python 3 (hopefully no one is still using Python 2) which can make your code even easier to understand and less confusing: Type Hints.
We will get an idea on why type hinting is useful, how it can be applied, and check out some examples.
Table of Contents
Introduction
As you know, we don’t need to declare variable types while writing Python code — one of the characteristics of dynamically typed languages — which can make development faster. However, this can sometimes lead to ambiguity when it comes to functions signatures for instance. In case there is no proper documentation, you would have no idea what is the expected input and output from these functions unless you look in the implementation itself and try to figure it out.
Sounds like a waste of time doesn’t it? Now let’s see what we can do about that.
Do I Really Need Type Hints?
Even though your code will run just fine without type hints, I will argue that adopting this feature will make you more efficient while developing, and will make the life of other people working with your code much easier.
Think about this example:
class Person:
def __init__(self, name, age):
self._name = name
self._age = age
def eat(self):
...
def walk(self):
...
def work(self):
...
def do_stuff(persons):
for person in persons:
person.eat()
person.walk()
person.work()
The function do_stuff
is expected to take as input a list of Person
objects and call some methods for each one of them. However, when you are developing this function, your IDE doesn’t know that, so you won’t get any auto complete suggestions and you won’t know the signatures of these methods (imagine methods with more parameters). You will end up jumping back and forth between the class definition and your do_stuff
function just to get the name and parameters of the methods.
We have all been there, and we know how inconvenient this is.
So let’s see how we can make use of type hints to have a smoother coding experience.
How Can I Use Them?
The syntax for annotating functions with type hints is very simple, here is how it looks like:
def print_info(name: str, age: int) -> str:
print(f"{name} is {age} years old")
We specified the type of name
as a string, and age
as integer. The return type is defined after the ->
which will be a string in our example.
The standard library introduced the module typing
to define the fundamental building blocks to construct types, here are some of them:
- List
Used asList[element_type]
- Dict
Used asDict[key_type, value_type]
- Set
Used asSet[element_type]
- Tuple
Used by listing the elements types (e.g.Tuple[int, int, str]
) - Union
Used to define a set of accepted types for a given parameter (e.g.Union[int, List[int]]
means that the argument can either be anint
or a list ofint
) - Optional
Used asOptional[element_type]
to define optional parameters, which is equivalent toUnion[element_type, None]
- Any
A special type which is compatible with every other type
I recommend you check out the complete PEP484 guide to get more detailed information about this.
Is There Something Else I Should Know?
A couple of things.
Circular imports
When I started using type hints, I ended up with this situation:
# controller.py
from model import Model
class Controller:
def __init__(self, model: Model): ...
# model.py
from controller import Controller
class Model:
def subscribe(controller: Controller): ...
The above code snippet doesn’t work due to the circular import between the Model
and Controller
classes. Luckily there is a simple solution to this pickle, by modifying our code a little bit so it looks like that:
# controller.py
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from model import Model
class Controller:
def __init__(self, model: Model): ...
# model.py
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from controller import Controller
class Model:
def subscribe(controller: Controller): ...
The from __future__ import annotations
line transforms all annotations to a string form (it has to be written in the top of the module). The TYPE_CHECKING
import is a special constant from the typing
module which is set to False
during runtime, but assumed to be True
by static type checkers.
In other words, the code will be equivalent to this:
# controller.py
class Controller:
def __init__(self, model: "Model"): ...
# model.py
class Model:
def subscribe(self, controller: "Controller"): ...
Local variables annotations
It’s also possible to add type hints for local variables
def do_stuff():
student: Person = get_student("foobar")
I want to leave you with one final note, quoted from PEP484:
Python will remain a dynamically typed language, and the authors have no desire to ever make type hints mandatory, even by convention
These type checks are not enforced by default, this needs to be done by an external static type checker like mypy
And that’s a wrap. Thank you for making it all the way to the end.
Till next time!