Skip to main content

4 levels of mastering type annotations in Python

·1083 words·6 mins
Grzegorz Kocjan
Author
Grzegorz Kocjan

Type annotations have been with us, Python developers, for a long time. They are used in many ways, sometimes really simple, sometimes to achieve complex behaviours.

Understanding and mastering type annotations in Python has become more significant as projects grow in size and complexity. Therefore, here are 4 levels of mastering type annotations that you can use to hint at your future growth and how to move forward with your project.

In the comments, tell me what level your project is and if you want to move to the next one (I hope you do)!

Level 0 - No Annotations At All
#

Before 2015, all Python projects were at this level, devoid of any type annotations. However, this changed with the release of Python 3.5, which introduced type annotations to the language.

In this initial level, type annotations are absent. This level is good for you if you plan to travel back to when Python was not supporting it. Otherwise, stop fooling around and move to the next level!

The primary advantage at this level is that you do not need any additional effort. You can just do what you are paid for and go playing games. Also, by not using type annotations, you can secure your job. Nowadays, nobody will join your project; therefore, no one will ever replace you.

But on the other side, karma will catch up with you eventually. The lack of type-checking can lead to bugs that are hard to trace. It is easier to write messy and unmaintainable code.

Level 1 - Standard Types
#

The first level is suitable for beginners or early-stage projects when the code is unstable or a proof of concept (POC) that will be discarded later.

At this level, basic standard types are utilized for annotations. For example:

def greet(person: dict) -> str:
    return "Hello, "+ person["name"]

alice = {"name": "Alice", age: 30)
greet(alice)

This level is easy to use, with plenty of online examples. It doesn’t require advanced programming skills, making it a friendly starting point for newcomers to Python.

However, the static type checker, Mypy, works only on a basic level here, which might only catch some fundamental errors. It’s not a suitable approach for long-term projects as they grow in complexity.

Level 2 - Dataclasses/Pydantic
#

Level 2 offers a stability boost, making it a good fit in the beginning and for mature projects.

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int

def greet(person: Person) -> str:
    return f"Hello, {person.name}"

alice = Person("Alice", 30)
greet(alice)

In this level, dict types are replaced with classes, providing a better structure and type checking. Here, your IDE will be super helpful with precise code syntax suggestions. Also, mypy works at a business logic level, catching more potential type errors before runtime or any tests.

A little boilerplate code might be involved, but it’s a worthy trade-off for the benefits garnered.

Here, you can choose between dataclasses or Pydantic. While dataclasses are built in the language, Pydantic is becoming increasingly popular, especially with new 2.0 versions, that is way faster.

Migrating to this level is easy. Introduce classes that represent your data in the project and use them. For a large codebase, you can start bottom-up from low-level functions and methods. You might discover that there is a lot of data inconsistency. That wouldn’t happen if you started from this level from the beginning, but now you need to learn how to refactor your code to not lose your mind while migrating. But that’s another topic, and let me know if I should cover it also in future posts.

Level 3 - No Standard Types
#

Level 3 is where you transcend standard types, crafting custom types that mirror your business logic.

from pydantic import BaseModel


class Name(str):
    pass


class Age(int):
    pass


class Greeting(str):
    pass


class Person(BaseModel):
    name: Name
    age: Age


def greet(person: Person) -> Greeting:
    return Greeting(f"Hello, {person.name}")


alice = Person(name=Name("Alice"), age=Age(30))
greet(alice)

Instead of standard types like str for names and greetings, you create and use custom types like Name and Greeting. This makes the business logic in your code more explicit and less prone to silly mistakes.

Now, variables are used correctly within the business logic context, providing a more explicit code base and better static code analysis. There is no chance of mixing variables, and you gain massive confidence in what you are doing. It is also easier to discover poorly designed code structures. It is a code smell if some types are defined far from their usage. If your code feels terrible at this level, it is not the fault of using precise annotations but lousy design.

However, it requires a solid understanding of business logic, Python typing and OOP principles, which might pose a challenge for beginners.

OK, I admit, just by looking at this simple example, those custom types look like a lot of additional boilerplate in the code. But that’s only an illusion. In a real-life project, you create a custom type once and use it hundreds of times.

True story
#

On one project, a new feature prompted a transition from Level 2 to Level 3 of type annotations. This feature introduced a concept similar to an existing one, with identical data types but distinct business roles. It was crucial to ensure that functions specific to one concept wouldn’t mix with the other.

The move to Level 3, with precise types, provided a clear distinction between these concepts, ensuring the correct values were always passed. I gained confidence in my work, even if the nomenclature was strikingly similar and easy to mix up.

Service transformed into a developer’s delight. The code was highly readable, and every module and function was coherent, serving a defined purpose. This makes this service one of the most enjoyable to develop and expand.

Of course, this transition, coupled with other good practices like Test Driven Development (TDD), Dependency Inversion and actual refactoring, but again, that’s another story.

Summary
#

As we’ve journeyed through the levels of type annotations, it’s clear that they offer various benefits, from code readability to type safety.

Level 1 provides a gentle introduction for beginners, while Level 2 offers a stable ground for growing projects. Level 3, with its custom types, shines in projects with complex business logic. Identifying the right level for your project and advancing through the levels as your project matures can significantly enhance your code quality.

Remember to write a comment on what level your project is!



comments powered by Disqus