Chapter 44, Advanced
Advanced typing
Protocols, generics, and overloads allow the type checker to understand flexible code without you giving up the flexibility of Python.
Code for this chapter: avancado/cap44_tipagem_avancada.py
Protocol: structural typing
An Protocol describes a way: any class with the right methods works, without inheriting from anything. It's verifiable duck typing. With runtime_checkable, the isinstance it also works, but only checks for the presence of the methods:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Fechavel(Protocol):
def fechar(self) -> None: ...
class Arquivo:
def fechar(self) -> None:
print("arquivo fechado")
class Conexao:
def fechar(self) -> None:
print("conexão fechada")
def encerrar(recurso: Fechavel) -> None:
recurso.fechar()
for recurso in (Arquivo(), Conexao()):
encerrar(recurso)
print(isinstance(Arquivo(), Fechavel), isinstance(42, Fechavel))
arquivo fechado
conexão fechada
True False
The advantage over a ABC is the coupling: Arquivo and Conexao do not know Fechavel. Who defines the contract is who consumes, on their side, and this makes dependencies easier to swap and to mock in tests. When an object does not fulfill the contract, the checker reports:
from typing import Protocol
class Fechavel(Protocol):
def fechar(self) -> None: ...
class SemFechar:
pass
def encerrar(recurso: Fechavel) -> None:
recurso.fechar()
encerrar(SemFechar())
uv run mypy exemplos/tipos/protocolo_erro.py
exemplos/tipos/protocolo_erro.py:16: error: Argument 1 to "encerrar" has incompatible type "SemFechar"; expected "Fechavel" [arg-type]
Found 1 error in 1 file (checked 1 source file)
Generics with Python 3.12 syntax
A class or function generic works with any type and preserves information about it. Since Python 3.12, the syntax with brackets eliminates the TypeVar. The parentheses after T: restrict the accepted types:
class Pilha[T]:
def __init__(self) -> None:
self._itens: list[T] = []
def empilhar(self, item: T) -> None:
self._itens.append(item)
def desempilhar(self) -> T:
return self._itens.pop()
def __len__(self) -> int:
return len(self._itens)
def primeiro[T](itens: list[T]) -> T:
return itens[0]
def maximo[T: (int, float, str)](a: T, b: T) -> T:
return a if a >= b else b
pilha = Pilha[int]()
pilha.empilhar(1)
pilha.empilhar(2)
print(pilha.desempilhar(), len(pilha), primeiro(["a", "b"]))
print(maximo(3, 7), maximo("a", "b"))
2 1 a
7 b
For the checker, Pilha[int] only accepts integers, and pilha.desempilhar() is a int, without conversion or comment.
Literal, Final, TypedDict, and Self
from typing import Final, Literal, NotRequired, Self, TypedDict
Modo = Literal["leitura", "escrita"]
LIMITE: Final = 3
class Opcoes(TypedDict):
modo: Modo
tentativas: NotRequired[int]
def abrir(opcoes: Opcoes) -> str:
return f"{opcoes['modo']} com {opcoes.get('tentativas', LIMITE)} tentativas"
class Construtor:
def __init__(self) -> None:
self.partes: list[str] = []
def adicionar(self, parte: str) -> Self:
self.partes.append(parte)
return self
print(abrir({"modo": "leitura"}))
print(Construtor().adicionar("a").adicionar("b").partes)
leitura com 3 tentativas
['a', 'b']
The Literal restricts to exact values, and the Self keeps the correct type in chainable methods, even in subclasses.
Overloading and typed decorators
The @overload declares different signatures for the same function, so the checker knows that dobrar(4) returns int and dobrar("ab") returns str. And a decorator that preserves the signature of the decorated function uses ParamSpec, which in the new syntax is written as **P:
import functools
from collections.abc import Callable
from typing import overload
@overload
def dobrar(x: int) -> int: ...
@overload
def dobrar(x: str) -> str: ...
def dobrar(x):
return x * 2
def registrar[**P, R](funcao: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(funcao)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"chamando {funcao.__name__}")
return funcao(*args, **kwargs)
return wrapper
@registrar
def somar(a: int, b: int) -> int:
return a + b
print(dobrar(4), dobrar("ab"))
print(somar(1, 2))
8 abab
chamando somar
3
Gradual typing
You don't need to type everything. I start with the boundaries: public functions, data models, and interfaces between modules. The checker enters CI in permissive mode and I tighten the rules gradually, module by module.
Exercise 1
A generic repository
Write Repositorio[T] with salvar(id_, item) and buscar(id_), that returns T | None.