overload en Python: firmas precisas

Publicado el: 29/08/2026
Tempo de leitura: 6 minutos
A person reads 'Python for Unix and Linux System Administration' indoors.

Una función Python puede aceptar varias combinaciones de argumentos y devolver tipos diferentes según la llamada. Una sola anotación con muchas uniones suele perder la relación entre entradas y salidas. typing.overload permite declarar varias firmas estáticas para una única implementación de runtime, mejorando inferencia y autocompletado sin duplicar la lógica.

Esta guía explica declaraciones overload, compatibilidad de implementación, Literal, None, genéricos, parámetros keyword-only, orden, solapamientos inseguros, métodos, Protocol, stubs y pruebas estáticas con mypy o pyright.

El problema de una unión amplia

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

resultado = buscar(2)
# el analizador puede ver int | str

En runtime, un int produce int y un str produce str. La anotación simple no conserva esa relación.

Primer overload

from typing import overload

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

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

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

Las funciones decoradas son declaraciones para el analizador. La última función sin decorador es la implementación ejecutada.

Las declaraciones no contienen lógica de runtime

Usa ... o un cuerpo vacío. No pongas comportamiento real en las variantes porque los consumidores no ejecutan esos cuerpos.

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

La implementación debe aparecer después de todas las variantes en el mismo bloque.

Compatibilidad de la implementación

La implementación debe aceptar todas las llamadas descritas y devolver todos los tipos prometidos.

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

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

Si una variante acepta bytes, la implementación no puede aceptar solo int. Si promete str, la ruta correspondiente no puede devolver None.

Overload con Literal

Literal es una de las combinaciones más útiles porque el resultado depende de una opción exacta.

from typing import Literal, overload

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

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

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

Con binario=True se obtienen bytes; con False o sin argumento se obtiene str.

Un bool no literal

opcion: bool = leer_configuracion()
resultado = cargar("datos.txt", binario=opcion)

La opción puede ser verdadera o falsa, por lo que el resultado correcto es str | bytes. Algunas APIs añaden una variante bool general después de las variantes Literal.

Orden de variantes

Coloca las variantes específicas antes de las generales. Una variante amplia al principio puede hacer inalcanzables las siguientes.

@overload
def analizar(valor: Literal["auto"]) -> ConfiguracionAutomatica: ...
@overload
def analizar(valor: str) -> Configuracion: ...

Literal["auto"] es más específico que str.

Variantes solapadas

Dos variantes pueden aceptar la misma llamada. Si sus retornos son incompatibles, la API queda ambigua.

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

Un int también es object. Ambas variantes coinciden, pero los retornos difieren. El solapamiento debe ser intencional y seguro. Prefiere retornos compatibles o rediseña la interfaz.

bool es subtipo de int

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

Coloca bool primero. La implementación también debe comprobar bool antes de int si el comportamiento es distinto.

Overload con None

@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()

El analizador conserva None para entrada None y str para entrada string.

Defaults y argumentos omitidos

Las APIs que distinguen argumento omitido de None explícito suelen necesitar una sentinela privada.

from typing import TypeVar

T = TypeVar("T")
_AUSENTE = object()

@overload
def leer_opcion(nombre: str) -> str: ...
@overload
def leer_opcion(nombre: str, default: T) -> str | T: ...

def leer_opcion(nombre: str, default: object = _AUSENTE) -> object:
    if nombre in configuracion:
        return configuracion[nombre]
    if default is _AUSENTE:
        raise KeyError(nombre)
    return default

La sentinela evita confundir “no proporcionado” con “proporcionado como None”.

Overload genérico

from collections.abc import Iterable
from typing import TypeVar

T = TypeVar("T")

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

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

Quizá una sola firma genérica con Iterable sea suficiente. No uses overload cuando TypeVar expresa la relación de forma más simple.

Cuándo TypeVar reemplaza overload

T = TypeVar("T")

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

No hace falta una variante para int, str, bytes y cada tipo futuro. Overload es para formas de llamada realmente distintas.

Conservar tipos de secuencia

@overload
def cortar(datos: str, inicio: int, fin: int) -> str: ...
@overload
def cortar(datos: bytes, inicio: int, fin: int) -> bytes: ...

def cortar(datos: str | bytes, inicio: int, fin: int) -> str | bytes:
    return datos[inicio:fin]

Un TypeVar restringido también puede expresar esta relación. Elige la forma más clara y compatible.

Parámetros keyword-only

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

Las variantes deben conservar la naturaleza keyword-only y la implementación necesita una interfaz compatible.

Los nombres de parámetros importan

Los nombres forman parte de llamadas por keyword. Mantenlos consistentes.

@overload
def abrir(origen: str) -> Recurso: ...
@overload
def abrir(origen: Path) -> Recurso: ...

Usar nombres distintos para la misma posición confunde y puede generar errores.

Métodos sobrecargados

class Cache:
    @overload
    def obtener(self, clave: str) -> object: ...

    @overload
    def obtener(self, clave: str, default: T) -> object | T: ...

    def obtener(self, clave: str, default: object = _AUSENTE) -> object:
        ...

self aparece en todas las variantes. Para classmethods y staticmethods, aplica los decoradores de forma consistente según el analizador.

Overload y Self

Factories pueden usar overloads cuando opciones diferentes producen tipos distintos. Si un método simplemente devuelve la clase concreta actual, typing.Self suele ser más simple. Consulta typing.Self en Python.

Overload en Protocol

from typing import Protocol

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

Una implementación estructural debe ser compatible con el conjunto completo.

Overload en stubs

Los archivos .pyi no contienen la implementación Python normal. Las variantes pueden representar toda la interfaz pública de una función implementada en C, generada dinámicamente o difícil de anotar.

Comportamiento en runtime

Las variantes no son llamadas normalmente. La implementación final reemplaza el nombre. Versiones modernas ofrecen introspección como get_overloads(), principalmente para herramientas.

from typing import get_overloads

variantes = get_overloads(cargar)

No bases la lógica principal en ese registro; overload sigue siendo una construcción estática.

Overload frente a singledispatch

functools.singledispatch selecciona implementaciones en runtime según el tipo del primer argumento. overload solo describe firmas al analizador. Resuelven problemas distintos.

Overload no implementa despacho

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

Sin una implementación posterior no existe una función útil. El decorador no elige automáticamente una variante.

Decoradores y overload

Un decorador aplicado a una API sobrecargada debe conservar todas las firmas. ParamSpec ayuda en decoradores genéricos; APIs complejas pueden necesitar overloads explícitos. Consulta ParamSpec en Python.

Retorno dependiente de dos 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 incompatibles")
    return a + b

La implementación acepta una unión amplia que incluye combinaciones inválidas y las rechaza en runtime. Es común siempre que todas las llamadas declaradas sean válidas.

Documentación

Las herramientas pueden mostrar la implementación o las variantes. Escribe una docstring que explique la relación entre argumentos y retornos, errores, efectos secundarios y validación.

Demasiadas variantes

Grandes conjuntos aumentan mantenimiento y coste del analizador. Si necesitas decenas de combinaciones, considera objetos de configuración, métodos separados, builders, genéricos o una API más explícita.

Errores comunes

  • Olvidar la implementación: overload no crea despacho.
  • Poner lógica en las variantes: los consumidores ejecutan la implementación final.
  • Usar una implementación incompatible: algunas llamadas prometidas no funcionan.
  • Colocar la variante general primero: las específicas pueden no coincidir.
  • Crear solapamientos inseguros: la inferencia queda ambigua.
  • Usar overload donde basta TypeVar: la API se vuelve repetitiva.

Ejemplo completo: deserialización por formato

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

@dataclass
class Configuracion:
    nombre: str
    activo: bool

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

@overload
def deserializar(
    datos: bytes,
    *,
    formato: Literal["binario"],
) -> Configuracion: ...

def deserializar(
    datos: str | bytes,
    *,
    formato: Literal["json", "binario"],
) -> dict[str, object] | Configuracion:
    if formato == "json":
        if not isinstance(datos, str):
            raise TypeError("json exige str")
        resultado = json.loads(datos)
        if not isinstance(resultado, dict):
            raise ValueError("se esperaba objeto JSON")
        return resultado

    if not isinstance(datos, bytes):
        raise TypeError("binario exige bytes")
    nombre, activo = decodificar_registro(datos)
    return Configuracion(nombre=nombre, activo=activo)

Las variantes exponen solo combinaciones válidas y retornos precisos. La implementación valida en runtime porque el código no tipado puede saltarse el análisis.

Probar overloads

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

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

Mantén fixtures con llamadas válidas e inválidas y ejecuta mypy o pyright en CI. Las pruebas de runtime deben cubrir la implementación y combinaciones dinámicas inválidas.

Buenas prácticas

Empieza por el contrato del consumidor. Usa pocas variantes específicas. Ordénalas de más específica a más general. Mantén nombres y defaults consistentes. Asegura que la implementación cubra todas. Prefiere TypeVar, Protocol, ParamSpec o métodos separados cuando sean más simples.

Conclusión

typing.overload describe varias firmas estáticas para una implementación y conserva relaciones precisas entre argumentos y retornos. Es especialmente útil con Literal, None, formatos alternativos, defaults y APIs de compatibilidad.

La documentación oficial de overload en Python define las reglas. Úsalo para llamadas realmente distintas, no como sustituto de un diseño simple o de validación en runtime.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

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

    TypeAliasType en Python: alias en runtime

    Aprende TypeAliasType en Python para crear alias explícitos, inspeccionarlos en runtime y modelar APIs genéricas reutilizables.

    Ler mais

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

    ClassVar en Python: separa clase e instancia

    Aprende ClassVar en Python para separar atributos de clase e instancia en dataclasses, registries, caches, herencia y contadores.

    Ler mais

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

    Final en Python: protege constantes y herencia

    Aprende Final y @final en Python para proteger constantes, atributos, métodos y clases, comprendiendo los límites en runtime.

    Ler mais

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

    Annotated en Python: tipos con metadatos

    Aprende Annotated en Python para añadir metadatos a tipos, crear validación, schemas, unidades e integraciones con frameworks.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Detailed view of programming code in a dark theme on a computer screen.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    NewType en Python: separa identificadores

    Aprende NewType en Python para separar IDs, códigos y valores primitivos, validar fronteras y evitar mezclas de dominio.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Vivid close-up of code on a computer screen showcasing programming details.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Never en Python: marca código inalcanzable

    Aprende typing.Never en Python para funciones sin retorno, código inalcanzable y exhaustividad con assert_never, Literal y Enum.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026