overload no Python: assinaturas precisas

Publicado em: 29/08/2026
Tempo de leitura: 8 minutos
High-angle view of woman coding on a laptop, with a Python book nearby. Ideal for programming and tech content.

Uma função Python pode aceitar combinações diferentes de argumentos e devolver tipos diferentes conforme a chamada. Uma única anotação com muitas uniões frequentemente perde a relação entre entrada e saída. typing.overload permite declarar várias assinaturas estáticas para a mesma implementação, oferecendo autocomplete e inferência mais precisos sem duplicar a lógica de runtime.

Neste guia, você aprenderá a escrever overloads corretos, separar declarações da implementação, usar Literal, None, genéricos e parâmetros nomeados, ordenar variantes, evitar sobreposições inseguras e testar o comportamento com mypy ou pyright.

O problema de uma união ampla

def buscar(chave: int | str) -> int | str:
    if isinstance(chave, int):
        return chave * 10
    return chave.upper()

resultado = buscar(2)
# o analisador pode enxergar int | str

Em runtime, uma entrada int sempre produz int e uma entrada str sempre produz str. A anotação simples não preserva essa relação.

Primeiro overload

from typing import overload

@overload
def buscar(chave: int) -> int: ...

@overload
def buscar(chave: str) -> str: ...

def buscar(chave: int | str) -> int | str:
    if isinstance(chave, int):
        return chave * 10
    return chave.upper()

As funções decoradas com @overload são declarações para o analisador. A última função, sem o decorador, é a implementação executada.

As declarações não contêm lógica

Use ... ou um corpo vazio nas variantes. Não coloque comportamento real nelas, porque não são as funções chamadas pelo programa.

@overload
def converter(valor: bytes) -> str: ...

A implementação precisa aparecer depois de todas as variantes no mesmo bloco.

Compatibilidade da implementação

A implementação deve aceitar todas as chamadas descritas e devolver todos os resultados prometidos.

@overload
def converter(valor: int) -> str: ...
@overload
def converter(valor: bytes) -> str: ...

def converter(valor: int | bytes) -> str:
    if isinstance(valor, bytes):
        return valor.decode()
    return str(valor)

Se uma variante aceita bytes, a implementação não pode aceitar apenas int. Se uma variante promete str, a implementação não pode devolver None naquele caminho.

Overload com Literal

Literal é uma das combinações mais úteis porque o retorno depende de uma opção exata.

from typing import Literal, overload

@overload
def carregar(caminho: str, *, binario: Literal[False] = False) -> str: ...

@overload
def carregar(caminho: str, *, binario: Literal[True]) -> bytes: ...

def carregar(caminho: str, *, binario: bool = False) -> str | bytes:
    modo = "rb" if binario else "r"
    with open(caminho, modo) as arquivo:
        return arquivo.read()

Uma chamada com binario=True resulta em bytes; com False ou sem o argumento, resulta em str.

Chamador com bool não literal

opcao: bool = ler_configuracao()
resultado = carregar("dados.txt", binario=opcao)

Como o valor pode ser True ou False, o retorno correto é str | bytes. Algumas APIs adicionam uma terceira variante com bool para deixar esse caso explícito, mas ela deve ser ordenada depois das variantes Literal.

Ordem das variantes

Overloads mais específicos devem vir antes dos mais gerais. Uma variante ampla colocada primeiro pode tornar as seguintes inalcançáveis para o algoritmo de resolução.

@overload
def analisar(valor: Literal["auto"]) -> ConfiguracaoAutomatica: ...
@overload
def analisar(valor: str) -> Configuracao: ...

Literal["auto"] é mais específico que str e deve aparecer primeiro.

Variantes sobrepostas

Duas variantes podem aceitar a mesma chamada. Se elas prometem retornos incompatíveis, a API é ambígua.

@overload
def exemplo(valor: int) -> int: ...
@overload
def exemplo(valor: object) -> str: ...

Um int também é object. Para a entrada int, as duas variantes se aplicam, mas os retornos divergem. O analisador pode usar a primeira correspondência, porém a sobreposição precisa ser intencional e segura. Prefira retornos compatíveis ou redesenhe a assinatura.

bool é subtipo de int

@overload
def formatar(valor: bool) -> str: ...
@overload
def formatar(valor: int) -> bytes: ...

Como bool herda de int, a ordem é relevante. Coloque bool primeiro. A implementação também precisa testar bool antes de int se o comportamento for diferente.

Overload com None

from typing import overload

@overload
def normalizar(valor: None) -> None: ...
@overload
def normalizar(valor: str) -> str: ...

def normalizar(valor: str | None) -> str | None:
    if valor is None:
        return None
    return valor.strip().casefold()

O analisador preserva None quando a entrada é None e str quando a entrada é str.

Overload com valor padrão None

T = TypeVar("T")

@overload
def obter(chave: str) -> object: ...
@overload
def obter(chave: str, padrao: T) -> object | T: ...

Defaults e ausência de argumentos exigem cuidado. Muitas vezes é necessário usar um objeto sentinela privado na implementação para distinguir “argumento omitido” de “argumento fornecido como None”.

Sentinela para distinguir ausência

_AUSENTE = object()

@overload
def ler_opcao(nome: str) -> str: ...
@overload
def ler_opcao(nome: str, padrao: T) -> str | T: ...

def ler_opcao(nome: str, padrao: object = _AUSENTE) -> object:
    if nome in configuracao:
        return configuracao[nome]
    if padrao is _AUSENTE:
        raise KeyError(nome)
    return padrao

A sentinela evita confundir um default legítimo None com a ausência do argumento.

Overload genérico

from collections.abc import Iterable
from typing import TypeVar, overload

T = TypeVar("T")

@overload
def primeiro(valores: tuple[T, ...]) -> T: ...
@overload
def primeiro(valores: list[T]) -> T: ...

def primeiro(valores: Iterable[T]) -> T:
    return next(iter(valores))

Neste caso, talvez uma única assinatura genérica com Iterable já seja suficiente. Não use overload quando TypeVar expressa a relação com menos repetição.

Quando TypeVar substitui overload

T = TypeVar("T")

def identidade(valor: T) -> T:
    return valor

Escrever uma variante para int, str, bytes e cada novo tipo seria desnecessário. Use overload quando existem formas realmente diferentes, não para enumerar tipos arbitrários.

Overload com sequências

@overload
def fatiar(dados: str, inicio: int, fim: int) -> str: ...
@overload
def fatiar(dados: bytes, inicio: int, fim: int) -> bytes: ...

def fatiar(dados: str | bytes, inicio: int, fim: int) -> str | bytes:
    return dados[inicio:fim]

Também seria possível usar um TypeVar restrito, dependendo da clareza e do suporte do checker. Compare a manutenção das duas formas.

Parâmetros keyword-only

@overload
def consultar(id_: int, *, completo: Literal[False] = False) -> Resumo: ...
@overload
def consultar(id_: int, *, completo: Literal[True]) -> RegistroCompleto: ...

As variantes devem preservar a natureza keyword-only. A implementação precisa usar a mesma interface pública compatível.

Parâmetros posicionais e nomeados

Nomes de parâmetros importam em chamadas por keyword. Evite usar nomes diferentes entre variantes, pois isso confunde consumidores e alguns verificadores.

@overload
def abrir(origem: str) -> Arquivo: ...
@overload
def abrir(origem: Path) -> Arquivo: ...

Use o mesmo nome origem em todas as variantes e na implementação.

Overload em métodos

class Cache:
    @overload
    def obter(self, chave: str) -> object: ...

    @overload
    def obter(self, chave: str, padrao: T) -> object | T: ...

    def obter(self, chave: str, padrao: object = _AUSENTE) -> object:
        ...

self aparece em todas as variantes. Para classmethods, aplique os decoradores na ordem recomendada pelo checker e pela documentação, normalmente combinando @overload com @classmethod de forma consistente em todas as declarações e na implementação.

Overload e Self

Métodos de factory podem usar overloads quando opções diferentes produzem subclasses ou formatos distintos. Para métodos que simplesmente retornam a classe concreta, typing.Self geralmente é mais simples. Veja o guia de typing.Self no Python.

Overload em Protocol

Protocol pode declarar uma callable ou método sobrecarregado sem implementação:

from typing import Protocol

class Parser(Protocol):
    @overload
    def parse(self, dados: str) -> Documento: ...
    @overload
    def parse(self, dados: bytes) -> DocumentoBinario: ...

Uma implementação estrutural precisa ser compatível com o conjunto de assinaturas.

Overload em stubs

Arquivos .pyi não contêm a implementação Python normal. Neles, as variantes com overload representam toda a interface pública. Essa é uma aplicação central para bibliotecas que possuem runtime em C, comportamento dinâmico ou implementação difícil de anotar diretamente.

O runtime das declarações

As variantes não são chamadas normalmente. A implementação final substitui o nome no módulo. O módulo typing oferece funções de introspecção como get_overloads() em versões modernas, mas overload continua sendo principalmente uma construção estática.

from typing import get_overloads

variantes = get_overloads(carregar)

Não construa a lógica principal dependendo dessa introspecção; ela é mais útil para ferramentas e testes especializados.

Overload e singledispatch

functools.singledispatch escolhe implementações em runtime com base no tipo do primeiro argumento. overload apenas descreve assinaturas ao analisador. É possível usar ambos, mas eles resolvem problemas diferentes. Consulte o site por guias de dispatch quando o comportamento realmente precisa de registro dinâmico.

Overload não implementa despacho

@overload
def processar(valor: int) -> int: ...
@overload
def processar(valor: str) -> str: ...

Sem uma implementação posterior, não existe função útil para executar. O decorador não escolhe automaticamente uma variante em runtime.

Decoradores e overload

Um decorador aplicado a uma função sobrecarregada precisa preservar todas as assinaturas. ParamSpec pode ajudar em decoradores genéricos, mas APIs muito complexas podem exigir overloads explícitos no decorador. Veja o guia de ParamSpec no Python.

Retorno dependente de dois argumentos

@overload
def combinar(a: str, b: str) -> str: ...
@overload
def combinar(a: bytes, b: bytes) -> bytes: ...

def combinar(a: str | bytes, b: str | bytes) -> str | bytes:
    if type(a) is not type(b):
        raise TypeError("tipos incompatíveis")
    return a + b

A implementação aceita uma união mais ampla que inclui combinações inválidas e as rejeita em runtime. Isso é comum, desde que todas as chamadas declaradas sejam aceitas e o corpo trate os casos extras conscientemente.

Implementação ampla demais

Uma implementação pode precisar aceitar uma forma ampla para satisfazer todas as variantes, mas não exponha combinações inválidas em sua assinatura pública se o checker as considerar chamáveis. Alguns analisadores usam apenas as variantes para consumidores externos, porém ainda verificam a implementação separadamente.

Documentação

Geradores de documentação podem mostrar a implementação ou as variantes dependendo da ferramenta. Escreva docstrings que expliquem a relação entre argumentos e retornos. Não confie apenas na lista de overloads para comunicar erros, efeitos colaterais e semântica.

Quantidade de variantes

Muitos overloads aumentam o custo de manutenção e análise. Se dezenas de combinações são necessárias, considere objetos de configuração, métodos separados, builders, genéricos ou uma API mais explícita.

Erros comuns

  • Esquecer a implementação: overload não cria despacho de runtime.
  • Colocar lógica nas variantes: o corpo não será usado como esperado.
  • Usar uma implementação incompatível: nem todas as chamadas prometidas são aceitas.
  • Ordenar a variante geral primeiro: as específicas podem ficar inalcançáveis.
  • Criar sobreposições com retornos incompatíveis: a inferência fica ambígua.
  • Usar overload onde TypeVar basta: a API fica repetitiva.

Exemplo completo: desserialização por formato

from dataclasses import dataclass
from typing import Literal, overload
import json

@dataclass
class Configuracao:
    nome: str
    ativo: bool

@overload
def desserializar(
    dados: str,
    *,
    formato: Literal["json"],
) -> dict[str, object]: ...

@overload
def desserializar(
    dados: bytes,
    *,
    formato: Literal["binario"],
) -> Configuracao: ...

def desserializar(
    dados: str | bytes,
    *,
    formato: Literal["json", "binario"],
) -> dict[str, object] | Configuracao:
    if formato == "json":
        if not isinstance(dados, str):
            raise TypeError("json exige str")
        resultado = json.loads(dados)
        if not isinstance(resultado, dict):
            raise ValueError("objeto JSON esperado")
        return resultado

    if not isinstance(dados, bytes):
        raise TypeError("binário exige bytes")
    nome, ativo = decodificar_registro(dados)
    return Configuracao(nome=nome, ativo=ativo)

As variantes expõem apenas combinações válidas e retornos precisos. A implementação valida a relação em runtime porque chamadas dinâmicas ainda podem ignorar o analisador.

Testando overloads

Use reveal_type() ou funções equivalentes em fixtures:

texto = carregar("a.txt")
reveal_type(texto)  # str

binario = carregar("a.bin", binario=True)
reveal_type(binario)  # bytes

Inclua chamadas que devem falhar e execute mypy ou pyright no CI. Testes de runtime devem verificar a implementação, especialmente combinações inválidas que podem chegar de código não tipado.

Boas práticas

Comece pelo contrato do chamador. Use poucas variantes específicas. Ordene do mais específico ao geral. Mantenha nomes e defaults consistentes. Garanta que a implementação cubra todas as declarações. Prefira TypeVar, Protocol, ParamSpec ou métodos separados quando expressarem a relação com mais simplicidade.

Conclusão

typing.overload descreve múltiplas assinaturas estáticas para uma única implementação e preserva relações precisas entre entradas e retornos. Ele é especialmente útil com Literal, None, formatos alternativos, defaults e APIs compatíveis com comportamentos históricos.

A documentação oficial de overload no Python detalha as regras. Use a ferramenta para representar chamadas realmente distintas, não como substituto de um design simples ou de validação em runtime.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    Close-up view of a computer screen displaying code in a software development environment.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    ClassVar no Python: separe classe e instância

    Aprenda ClassVar no Python para separar atributos de classe e instância em dataclasses, registries, caches, herança e contadores.

    Ler mais

    Tempo de leitura: 7 minutos
    29/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Final no Python: proteja constantes e herança

    Aprenda Final e @final no Python para proteger constantes, atributos, métodos e classes, entendendo os limites em runtime.

    Ler mais

    Tempo de leitura: 7 minutos
    29/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Annotated no Python: tipos com metadados

    Aprenda Annotated no Python para adicionar metadados a tipos, criar validação, schemas, unidades e integrações com frameworks.

    Ler mais

    Tempo de leitura: 7 minutos
    29/08/2026
    Laptop displaying code editor on a desk with a coffee mug beside it, suggesting a workspace or home office setting.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    NewType no Python: IDs sem misturar

    Aprenda NewType no Python para separar IDs, códigos e valores primitivos, validar fronteiras e evitar misturas sem criar classes pesadas.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Never no Python: marque código inalcançável

    Aprenda typing.Never no Python para funções que não retornam, código inalcançável e verificação exaustiva com assert_never.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Macro shot capturing detailed patterns of a python in its natural surroundings.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Concatenate no Python: altere parâmetros

    Aprenda Concatenate no Python para adicionar ou ocultar parâmetros em decoradores tipados com ParamSpec, contexto e dependências.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026