Yet Another Way To Improve Python Code Readability

Ahmed Ghazal
4 min readApr 9, 2023

Photo by Agence Olloweb on Unsplash

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 as List[element_type]
  • Dict
    Used as Dict[key_type, value_type]
  • Set
    Used as Set[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 an int or a list of int)
  • Optional
    Used as Optional[element_type] to define optional parameters, which is equivalent to Union[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!

References

Sign up to discover human stories that deepen your understanding of the world.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Ahmed Ghazal
Ahmed Ghazal

Written by Ahmed Ghazal

Data Engineer | Python Enthusiast

No responses yet

Write a response