Post

mypy: how to use it in my project? Part 3: kick-ass tools that leverage type annotations

Type annotations are a formalized way to add some extra information about types to your project. Once you get through adding mypy to your project and annotate your code (remember you can do it automatically, at least to some extent) you will find yourself at the ocean of possibilities.

This post will show the most impressive libraries that leverage type hints that I know.

Type checkers

If you have read previous articles in the series you know by now that official tool for validating types in python is called mypy. But have you heard there are alternatives?

First of them is pyre - a tool that was originally developed by Facebook. Unlike Python-based mypy, it is written in OCaml. pyre is not meant to replace mypy as such. It is rather to be used as a part of Facebook ecosystem. Similar to other projects built in the past for Hack/PHP, it is an extendable "framework" to build more static-code analysis tools atop it. Thanks to being written in another language, it is expected to be faster than mypy. For more details, check this answer on Hackernews and Pycon 2018 talk Types, Deeper static analysis and you by Pieter Hooimejier.

Another tool, this time coming from Microsoft, is pyright. It is written in TypeScript hence requires node js to work. It does not seem to provide any extra features compared to mypy though certainly there are differences in the way how both tools work. Here's a documentation page on how the internals of pyright work.

Classes without boilerplate - dataclasses & attrs

A very dull part of defining stateful classes is writing their __init__ method that will initialize the instance.

class User:
    def __init__(self, id: int, first_name: str, last_name: str) -> None:
        self.id = id
        self.first_name = first_name
        self.last_name = last_name

A recognized Python library that relieves us from that boring task is attrs:

import attr

@attr.s(auto_attribs=True)
class User:
    id: int
    first_name: str
    last_name: str

attrs is very flexible and easy to work with. It does not provide any validation out of the box but makes it easy to build our own. Check out its documentation for full details.

A poor man's replacement for attrs is dataclasses module, bundled with Python standard library since Python 3.7. There are available backports for older versions. It provides very limited functionality compared to attrs, though it has everything that is needed for simple use cases:

from dataclasses import dataclass

@dataclass
class User:
    id: int
    first_name: str
    last_name: str

Data modelling & validation - pydantic

Data validation and settings management using python type annotations.

Pydantic enables us to model our data in a nice, declarative way using just type annotations. In return, it will convert and validate data for us. Let's look at a slightly modified example from its documentation:

from datetime import datetime
from typing import List
from pydantic import BaseModel

class User(BaseModel):
    id: int
    name = 'John Doe'
    signup_ts: datetime = None
    friends: List[int] = []

external_data = {
    'id': '123',
    'signup_ts': '2019-06-01 12:22',
    'friends': [1, 2, '3']
}
user = User(**external_data)
print(user)
# User(id=123, signup_ts=datetime.datetime(2019, 6, 1, 12, 22), friends=[1, 2, 3], name='John Doe')
# {'id': 123, 'signup_ts': datetime.datetime(2019, 6, 1, 12, 22), 'friends': [1, 2, 3], 'name': 'John Doe'}

Pydantic is extensively used by FastAPI framework. It looks like a modern replacement for other serialization/deserialization libraries for web, like marshmallow or colander.

Dependency injection with python-injector

Injector is Guice-inspired dependency injection library. Simply saying, it is able to construct complex object graphs (resolve dependencies). For example, let's say we need class PlacingBidUseCase which is defined as follows:

class PlacingBidUseCase:
    def __init__(
        self, output_boundary: PlacingBidOutputBoundary, auctions_repo: AuctionsRepository) -> None:
        self.output_boundary = output_boundary
        self.auctions_repo = auctions_repo

By looking at its __init__ we see it requires an instance of PlacingBidOutputBoundary and AuctionsRepository. Building them manually each time we need an instance of PlacingBidUseCase would be impractical, so we configure python-injector and just ask it to give us PlacingBidUseCase class.

The library has nice integration with Flask, called simply flask-injector.

It is extremely helpful for Python projects that use the Clean Architecture.

Types validation in runtime

Last but not least - there is a whole class of libraries that provide a run-time validation of types. For the sake of example, I will show just one of them - typeguard. In the simplest form, one can use typechecked decorator to verify arguments and return types:

from typeguard import typechecked

@typechecked
def is_a_bigger_than_b(a: int, b: int) -> bool:
    return a > b

is_a_bigger_than_b('a', 'b')
Traceback (most recent call last):
  ...
TypeError: type of argument "a" must be int; got str instead

Bear in mind such checks can be very expensive. Unlike statically-typed languages, Python will not validate types before run-time which means calling isinstance builtin function. These tools should not be recklessly applied everywhere in the project.

Further reading

There is a Github repository awesome-python-typing that is meant to list all awesome tools built upon type hints.

What about you? Are you using any other cool tool built atop type annotations? If so, share it in comments :)

This post is licensed under CC BY 4.0 by the author.

Comments powered by Disqus.