Voltar ao Glossario
Glossario Python

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.

Termos Relacionados

  • FastAPI - Framework que depende de type hints
  • Pydantic - Validação de dados com type hints
  • Dataclass - Classes de dados com tipagem