Type Hints: O que São e Como Funcionam | Python Brasil
Type hints em Python: TypeVar, Protocol, Literal, TypedDict, Annotated, verificação com mypy e pyright, stub files, e a sintaxe do Python 3.12.
O que são Type Hints?
Type hints (dicas de tipo) são anotações que indicam os tipos esperados de variáveis, parâmetros e retornos de funções em Python. Introduzidos no Python 3.5 (PEP 484), eles não alteram o comportamento do código em tempo de execução — o Python continua sendo uma linguagem de tipagem dinâmica — mas habilitam a detecção estática de bugs por ferramentas como mypy e pyright, além de melhorar enormemente o autocompletar em editores como VS Code e PyCharm.
Como usar: sintaxe básica
# Sem type hints (difícil de entender sem ler a implementação)
def calcular_desconto(preco, percentual):
return preco * (1 - percentual / 100)
# Com type hints (autodocumentado)
def calcular_desconto(preco: float, percentual: float) -> float:
return preco * (1 - percentual / 100)
# Variáveis tipadas
nome: str = "Python"
idade: int = 33
ativo: bool = True
preco: float = 29.90
# Tipos compostos nativos (Python 3.9+: sem importar typing)
nomes: list[str] = ["Ana", "Bruno"]
mapeamento: dict[str, int] = {"python": 1991}
coordenadas: tuple[float, float] = (23.5, -46.6)
conjunto: set[int] = {1, 2, 3}
# Union types: Python 3.10+ usa | em vez de Union
def formatar(valor: int | float | str) -> str:
return str(valor)
# Optional (equivale a X | None)
from typing import Optional
def buscar_usuario(user_id: int) -> Optional[dict]:
usuarios = {1: {"nome": "Ana"}}
return usuarios.get(user_id)
# Python 3.10+: equivalente mais limpo
def buscar_usuario_moderno(user_id: int) -> dict | None:
usuarios = {1: {"nome": "Ana"}}
return usuarios.get(user_id)
O módulo typing em profundidade
TypeVar: generics simples
from typing import TypeVar, Generic
T = TypeVar('T') # Variável de tipo genérico
def primeiro_elemento(lista: list[T]) -> T:
"""Retorna o primeiro elemento, preservando o tipo."""
return lista[0]
resultado_str = primeiro_elemento(["a", "b", "c"]) # str
resultado_int = primeiro_elemento([1, 2, 3]) # int
# TypeVar com restrições
Numerico = TypeVar('Numerico', int, float)
def dobrar(x: Numerico) -> Numerico:
return x * 2
Generic: classes genéricas
from typing import Generic, TypeVar
T = TypeVar('T')
class Pilha(Generic[T]):
def __init__(self) -> None:
self._itens: list[T] = []
def empilhar(self, item: T) -> None:
self._itens.append(item)
def desempilhar(self) -> T:
if not self._itens:
raise IndexError("Pilha vazia")
return self._itens.pop()
def topo(self) -> T:
return self._itens[-1]
pilha_int: Pilha[int] = Pilha()
pilha_int.empilhar(1)
pilha_int.empilhar(2)
print(pilha_int.desempilhar()) # 2, tipado como int
Protocol: tipagem estrutural (duck typing com tipos)
Protocol permite definir interfaces baseadas em comportamento, sem herança explícita — o verdadeiro duck typing com suporte estático:
from typing import Protocol, runtime_checkable
@runtime_checkable # Permite usar isinstance() em tempo de execução
class Serializavel(Protocol):
def to_dict(self) -> dict: ...
def to_json(self) -> str: ...
class Usuario:
def __init__(self, nome: str, email: str):
self.nome = nome
self.email = email
def to_dict(self) -> dict:
return {'nome': self.nome, 'email': self.email}
def to_json(self) -> str:
import json
return json.dumps(self.to_dict())
# Usuario satisfaz o Protocol sem herdar dele explicitamente
def salvar(obj: Serializavel) -> None:
dados = obj.to_dict()
print(f"Salvando: {dados}")
usuario = Usuario("Ana", "ana@exemplo.com")
salvar(usuario) # Funciona! E o mypy verifica estaticamente.
print(isinstance(usuario, Serializavel)) # True (runtime_checkable)
Literal: valores específicos como tipos
from typing import Literal
# Apenas esses valores exatos são válidos
Direcao = Literal['norte', 'sul', 'leste', 'oeste']
HttpMethod = Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH']
Nivel = Literal[1, 2, 3, 4, 5]
def mover(direcao: Direcao, passos: int) -> None:
print(f"Movendo {passos} passos para o {direcao}")
def fazer_requisicao(url: str, metodo: HttpMethod = 'GET') -> None:
pass
mover('norte', 3) # OK
# mover('diagonal', 2) # mypy: error: "diagonal" não é Literal['norte', 'sul'...]
TypedDict: dicionários com estrutura definida
from typing import TypedDict, NotRequired
class Endereco(TypedDict):
rua: str
numero: int
bairro: str
cidade: str
cep: str
class Usuario(TypedDict):
id: int
nome: str
email: str
endereco: Endereco
telefone: NotRequired[str] # Campo opcional (Python 3.11+)
def processar_usuario(usuario: Usuario) -> str:
return f"{usuario['nome']} - {usuario['endereco']['cidade']}"
# O mypy verifica que todos os campos obrigatórios estão presentes
usuario: Usuario = {
'id': 1,
'nome': 'Ana',
'email': 'ana@exemplo.com',
'endereco': {
'rua': 'Rua das Flores',
'numero': 42,
'bairro': 'Centro',
'cidade': 'São Paulo',
'cep': '01001-000'
}
}
Annotated: metadados em tipos
from typing import Annotated
# Annotated[tipo, *metadados]: o tipo é o primeiro argumento
# os demais são metadados ignorados pelo Python, mas usados por ferramentas
# Pydantic usa Annotated para validações
from pydantic import Field
class Produto(BaseModel):
nome: Annotated[str, Field(min_length=2, max_length=100)]
preco: Annotated[float, Field(gt=0, description="Preço em reais")]
estoque: Annotated[int, Field(ge=0, le=10000)]
# Você pode criar seus próprios metadados
PositivoFloat = Annotated[float, "deve ser positivo"]
EmailStr = Annotated[str, "formato de email válido"]
ParamSpec: tipando decoradores que preservam assinatura
from typing import ParamSpec, TypeVar, Callable
from functools import wraps
import time
P = ParamSpec('P')
T = TypeVar('T')
def medir_tempo(func: Callable[P, T]) -> Callable[P, T]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
inicio = time.perf_counter()
resultado = func(*args, **kwargs)
duracao = time.perf_counter() - inicio
print(f"{func.__name__} levou {duracao:.4f}s")
return resultado
return wrapper
@medir_tempo
def processar(dados: list[int], fator: float = 1.0) -> list[float]:
return [x * fator for x in dados]
# O mypy sabe que processar ainda aceita (list[int], float) e retorna list[float]
resultado = processar([1, 2, 3], fator=2.0)
overload: múltiplas assinaturas para uma função
from typing import overload
@overload
def processar(dados: str) -> str: ...
@overload
def processar(dados: int) -> int: ...
@overload
def processar(dados: list) -> list: ...
def processar(dados):
"""Implementação real sem anotações de tipo."""
if isinstance(dados, str):
return dados.upper()
if isinstance(dados, int):
return dados * 2
return [x * 2 for x in dados]
# O mypy inferirá o tipo de retorno correto para cada chamada:
resultado_str: str = processar("hello") # str
resultado_int: int = processar(42) # int
resultado_list: list = processar([1, 2]) # list
Type narrowing
Type narrowing é quando o verificador de tipos infere um tipo mais específico dentro de um bloco condicional:
from typing import Union
def processar_valor(valor: int | str | None) -> str:
if valor is None:
return "vazio" # Aqui: valor é None
if isinstance(valor, int):
return str(valor * 2) # Aqui: valor é int
return valor.upper() # Aqui: valor é str (narrowed automaticamente)
# TypeGuard: narrowing personalizado
from typing import TypeGuard
def e_lista_de_strings(valor: list) -> TypeGuard[list[str]]:
return all(isinstance(item, str) for item in valor)
def processar_strings(dados: list) -> None:
if e_lista_de_strings(dados):
# Aqui: dados é list[str]
for item in dados:
print(item.upper()) # mypy sabe que item é str
Verificação estática: mypy vs pyright
mypy é a ferramenta de verificação de tipo original do Python, mantida pela equipe do Python.
pyright é a ferramenta da Microsoft, usada como base para o Pylance no VS Code. Tende a ser mais rápido e mais estrito.
# mypy
pip install mypy
mypy meu_arquivo.py
mypy meu_pacote/ --strict # Modo estrito: ativa todas as verificações
# pyright
pip install pyright
pyright meu_arquivo.py
# mypy.ini
[mypy]
python_version = 3.12
strict = true
ignore_missing_imports = true
[mypy-biblioteca_sem_tipos.*]
ignore_missing_imports = true
Stub files e py.typed
Para bibliotecas sem type hints no código-fonte, as anotações de tipo ficam em arquivos .pyi (stub files):
minha_biblioteca/
├── py.typed # Arquivo vazio: sinaliza que a biblioteca tem tipos embutidos
├── __init__.py
├── core.py
└── utils.pyi # Stub file para utils.py (se utils.py não tiver hints)
# utils.pyi - mesma estrutura do módulo, apenas com assinaturas
def calcular(x: float, y: float) -> float: ...
def formatar(valor: float, casas: int = ...) -> str: ...
class Calculadora:
def somar(self, a: float, b: float) -> float: ...
def dividir(self, a: float, b: float) -> float: ...
Verificação em tempo de execução
# typeguard: verifica tipos em tempo de execução
# pip install typeguard
from typeguard import typechecked
@typechecked
def somar(a: int, b: int) -> int:
return a + b
somar(1, 2) # OK
somar("a", "b") # TypeError em tempo de execução!
# beartype: alternativa mais rápida ao typeguard
# pip install beartype
from beartype import beartype
@beartype
def processar(dados: list[int]) -> list[float]:
return [float(x) for x in dados]
Python 3.12: nova sintaxe para generics
O Python 3.12 (PEP 695) introduziu uma sintaxe mais limpa para definir tipos genéricos:
# Python 3.11 e anterior
from typing import TypeVar, Generic
T = TypeVar('T')
def primeiro(lista: list[T]) -> T:
return lista[0]
class Pilha(Generic[T]):
def empilhar(self, item: T) -> None: ...
# Python 3.12+: sintaxe nativa para generics
def primeiro[T](lista: list[T]) -> T:
return lista[0]
class Pilha[T]:
def empilhar(self, item: T) -> None: ...
# TypeAlias também ficou mais limpo
# Python 3.11:
from typing import TypeAlias
Vetor: TypeAlias = list[float]
# Python 3.12+:
type Vetor = list[float]
type Matriz = list[list[float]]
Quando usar type hints
Type hints sao especialmente valiosos em codigo de biblioteca que sera usado por outros desenvolvedores, projetos grandes com muitos colaboradores, APIs publicas e interfaces entre modulos, e ao usar frameworks como FastAPI e Pydantic que dependem de anotacoes para funcionar. Em scripts curtos e de uso pessoal, type hints podem ser omitidos se tornarem o codigo mais verboso sem beneficio real.