Módulo 2 - Tipagem estática
Generics com TypeVar e Protocol
10 min de leitura · por Cesar Gargiulo, revisado pela equipe ValorFinal e GuardiaSec · Atualizado em 01/07/2026
O que você vai aprender
- Escrever funções genéricas com TypeVar preservando a relação de tipos.
- Entender por que um genérico é melhor que Any nessas situações.
- Definir um Protocol para aceitar objetos por seus métodos.
- Reconhecer o duck typing tipado que o Protocol traz.
Ouvir o resumo desta aula
Um recap de cerca de 2 minutos na voz do Valim, para ouvir no trânsito ou na academia.
Ler a transcrição do resumo
Resumo da aula: Generics com TypeVar e Protocol.
Os objetivos desta aula. Escrever funções genéricas com TypeVar preservando a relação de tipos. Entender por que um genérico é melhor que Any nessas situações. Definir um Protocol para aceitar objetos por seus métodos. Reconhecer o duck typing tipado que o Protocol traz.
Veja o essencial, parte por parte.
TypeVar: uma função para qualquer tipo. TypeVar cria um tipo reservado que é o mesmo em toda a assinatura.
Limitando o TypeVar. Se o tipo de saída depende do tipo de entrada, use um TypeVar para preservar a ligação.
Protocol: duck typing verificado. No Intermediário você viu o duck typing: se um objeto tem o método que você precisa, ele serve, não importa a classe dele.
Esse foi o resumo do essencial. Para se aprofundar, leia a aula completa e responda os exercícios.
TypeVar: uma função para qualquer tipo
Imagine uma função que devolve o primeiro item de uma lista. Ela funciona para lista de int, de str, de qualquer coisa, e o tipo que sai é o mesmo que entra: se a lista é de str, o primeiro item é str. Anotar isso com Any perderia essa relação, porque Any some com a informação. A solução é um TypeVar, uma variável de tipo. Você declara T = TypeVar('T') e usa T na assinatura. O T não é um tipo fixo; é um espaço reservado que representa o mesmo tipo em toda a função.
from typing import TypeVar
T = TypeVar("T")
def primeiro(itens: list[T]) -> T:
return itens[0]
n = primeiro([10, 20, 30]) # o mypy sabe: n e int
s = primeiro(["a", "b", "c"]) # o mypy sabe: s e str
print(n) # 10
print(s) # aT liga entrada e saída: lista de int devolve int, lista de str devolve str.
A diferença para Any é sutil e importante. Com list[Any] e retorno Any, o mypy não saberia o tipo do que sai, e você perderia autocompletar e verificação depois. Com list[T] e retorno T, o mypy conclui que primeiro de uma lista de inteiros devolve um inteiro. A relação entre entrada e saída fica preservada. Esse é o coração dos generics: código que serve para qualquer tipo sem abrir mão da verificação.
Limitando o TypeVar
Às vezes o tipo não pode ser qualquer um. Uma função que devolve o maior de dois valores só faz sentido para tipos que se comparam. Você pode restringir o TypeVar com bound, que fixa um limite superior, ou com uma lista de tipos permitidos. Com bound=int, o T aceita int e subtipos de int. Com uma restrição a tipos específicos, como TypeVar('N', int, float), o T só pode ser um daqueles listados. Isso mantém a flexibilidade dentro de um limite seguro.
from typing import TypeVar
N = TypeVar("N", int, float)
def maior(a: N, b: N) -> N:
return a if a > b else b
print(maior(3, 7)) # 7 (int)
print(maior(2.5, 1.5)) # 2.5 (float)
# maior("a", "b") seria apontado pelo mypy: str nao esta na listaN restrito a int e float: a função aceita esses tipos e nada mais.
Protocol: duck typing verificado
No Intermediário você viu o duck typing: se um objeto tem o método que você precisa, ele serve, não importa a classe dele. O problema é que isso não era verificável antes de rodar. O Protocol resolve. Você declara uma classe que herda de Protocol e lista os métodos e atributos esperados, sem implementação. Qualquer objeto que tenha esses métodos satisfaz o Protocol, mesmo sem herdar dele. O mypy passa a verificar isso: é o duck typing com rede de segurança.
from typing import Protocol
class TemArea(Protocol):
def area(self) -> float:
...
class Circulo:
def __init__(self, raio: float):
self.raio = raio
def area(self) -> float:
return 3.14159 * self.raio ** 2
class Quadrado:
def __init__(self, lado: float):
self.lado = lado
def area(self) -> float:
return self.lado ** 2
def maior_area(formas: list[TemArea]) -> float:
return max(forma.area() for forma in formas)
print(maior_area([Circulo(2), Quadrado(3)])) # 12.566...Circulo e Quadrado satisfazem TemArea sem herdar dele: basta ter area().
Nem Circulo nem Quadrado herdam de TemArea, e mesmo assim os dois são aceitos por maior_area, porque ambos têm o método area que devolve float. Isso é poderoso: você define contratos por comportamento, não por hierarquia de classes. É o jeito pythônico de tipar, alinhado com o duck typing, mas agora o mypy confere. Fecha assim o núcleo da tipagem estática; falta ver a ferramenta que roda toda essa verificação, o mypy, na próxima aula.
Teste rápido
Por que Circulo é aceito onde se espera um TemArea, mesmo sem herdar de TemArea?
Perguntas frequentes
- Qual a diferença entre usar TypeVar e usar Any na assinatura?
- TypeVar preserva a relação entre os tipos: se entra uma lista de str, o mypy sabe que sai str. Any apaga essa informação e você perde autocompletar e verificação depois. Sempre que a saída depender do tipo de entrada, o TypeVar é a escolha certa.
- Preciso registrar minhas classes para elas satisfazerem um Protocol?
- Não. É a grande vantagem do Protocol: qualquer objeto que tenha os métodos e atributos exigidos satisfaz o contrato automaticamente, sem herança nem registro. O mypy compara a forma do objeto com a do Protocol e aceita se combinar. É duck typing estrutural.
- O que significa o bound de um TypeVar?
- O bound fixa um limite superior para o tipo. Com TypeVar('T', bound=int), o T pode ser int ou qualquer subtipo de int, mas não str. Isso permite chamar métodos que todo subtipo do bound tem, mantendo a segurança. É útil quando o genérico precisa de certas operações.
- Aquele ... dentro do método do Protocol é código de verdade?
- É um corpo vazio simbólico. As reticências, ou um pass, indicam que o método não tem implementação no Protocol; ele só declara a assinatura esperada. As implementações reais ficam nas classes concretas que satisfazem o Protocol, como Circulo e Quadrado.
- Generics servem só para funções ou também para classes?
- Para ambos. Você pode criar classes genéricas que trabalham com um tipo parametrizado, como uma pilha de qualquer tipo. A ideia é a mesma do TypeVar em funções: preservar a relação de tipos. Este módulo foca no caso mais comum, funções genéricas, que já cobre bastante.
- Protocol substitui as classes base abstratas?
- Resolve muitos dos mesmos problemas de outra forma. A classe base abstrata exige herança explícita; o Protocol verifica por estrutura, sem herança. Cada um tem seu lugar: use Protocol quando quiser aceitar objetos de origens diferentes que apenas compartilham um comportamento.
Fontes
Seu progresso fica salvo neste aparelho. Assinantes sincronizam entre os aparelhos.