Skip to content

    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:

    avancado/cap44_tipagem_avancada.pylinhas 10 a 34
    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))
    
    Output
    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:

    exemplos/tipos/protocolo_erro.py
    from typing import Protocol
    
    
    class Fechavel(Protocol):
        def fechar(self) -> None: ...
    
    
    class SemFechar:
        pass
    
    
    def encerrar(recurso: Fechavel) -> None:
        recurso.fechar()
    
    
    encerrar(SemFechar())
    
    Terminal
    uv run mypy exemplos/tipos/protocolo_erro.py
    
    Output
    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:

    avancado/cap44_tipagem_avancada.pylinhas 39 a 65
    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"))
    
    Output
    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

    avancado/cap44_tipagem_avancada.pylinhas 70 a 95
    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)
    
    Output
    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:

    avancado/cap44_tipagem_avancada.pylinhas 100 a 128
    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))
    
    Output
    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.