singledispatch en Python: funciones por tipo

Publicado el: 25/07/2026
Tempo de leitura: 7 minutos
Código Python con funciones especializadas por tipo

En muchos proyectos de Python, una función pequeña termina acumulando bloques if para tratar enteros, cadenas, listas, diccionarios y objetos propios. Al principio, unas pocas comprobaciones con isinstance() parecen suficientes. Sin embargo, cuando aparecen nuevos tipos, la función mezcla demasiadas reglas y resulta más difícil de mantener y probar. singledispatch en Python ofrece una alternativa ordenada: una única función pública puede tener varias implementaciones, y Python selecciona la adecuada según el tipo del primer argumento.

En esta guía aprenderás a usar functools.singledispatch, definir una implementación predeterminada, registrar tipos concretos y abstractos, trabajar con clases propias y uniones, inspeccionar el registro, probar el despacho y reconocer cuándo conviene elegir otro patrón. El tema se relaciona directamente con las funciones en Python, los decoradores, la programación orientada a objetos y las dataclasses en Python.

¿Qué es singledispatch?

singledispatch es un decorador de la biblioteca estándar que convierte una función normal en una función genérica. Cuando se llama, la función genérica revisa el tipo real de su primer argumento y ejecuta la implementación registrada más específica. La expresión “single dispatch” indica que un solo argumento participa en la selección.

El diseño fue estandarizado mediante la PEP 443. La documentación oficial de functools.singledispatch explica el registro, la resolución por herencia, las uniones de tipos, las clases base abstractas y las herramientas de inspección.

Primer ejemplo con singledispatch

Supongamos que una aplicación necesita describir distintos valores. En lugar de concentrar todas las reglas en una cadena de condiciones, podemos separar cada implementación:

from functools import singledispatch

@singledispatch
def describir(valor):
    return f"Valor de tipo {type(valor).__name__}: {valor!r}"

@describir.register
def _(valor: int):
    return f"Número entero: {valor}"

@describir.register
def _(valor: str):
    return f"Texto con {len(valor)} caracteres"

@describir.register
def _(valor: list):
    return f"Lista con {len(valor)} elementos"

print(describir(10))
print(describir("Python"))
print(describir([1, 2, 3]))
print(describir({"activo": True}))

La primera función decorada es la implementación predeterminada. Se ejecuta cuando no existe un registro más específico. Cada bloque con @describir.register añade una especialización y mantiene el mismo nombre público para quien llama la función.

¿Por qué las funciones registradas suelen llamarse _?

Muchos ejemplos usan el nombre _ porque normalmente no se llama directamente a la implementación especializada. El acceso habitual se realiza mediante la función genérica. No obstante, puedes utilizar un nombre descriptivo:

@describir.register
def describir_float(valor: float):
    return f"Número decimal: {valor:.2f}"

El decorador de registro devuelve la función especializada sin reemplazarla, por lo que un nombre claro puede facilitar pruebas unitarias y depuración.

Registrar un tipo de forma explícita

Las anotaciones son cómodas, pero también puedes indicar el tipo directamente:

@describir.register(tuple)
def _(valor):
    return f"Tupla con {len(valor)} posiciones"

Esta forma resulta útil en código antiguo, cuando no se desean anotaciones o al registrar una clase importada desde otra biblioteca.

Cómo decide Python qué implementación usar

Python busca primero una coincidencia exacta para el tipo del primer argumento. Si no la encuentra, recorre la jerarquía de herencia y elige la clase registrada más cercana. Gracias a este comportamiento, las clases base abstractas son muy útiles.

from collections.abc import Mapping

@describir.register
def _(valor: Mapping):
    claves = list(valor)[:3]
    return f"Mapeo con claves: {claves}"

print(describir({"nombre": "Ana", "edad": 30}))

Un diccionario cumple la interfaz Mapping, por lo que esta implementación puede atenderlo. Registrar abstracciones como Mapping, Sequence o Set suele ser más flexible que limitar el código a una única clase concreta.

Usar clases propias

Las clases del dominio funcionan igual que los tipos incorporados:

from dataclasses import dataclass

@dataclass
class Producto:
    nombre: str
    precio: float

@describir.register
def _(valor: Producto):
    return f"Producto {valor.nombre}: ${valor.precio:.2f}"

producto = Producto("Teclado", 79.90)
print(describir(producto))

Este patrón puede servir para serializadores, presentadores, generadores de comandos, eventos, exportadores y adaptadores. Cuando aparece un nuevo tipo del dominio, se añade una implementación sin modificar una gran función central.

Registrar uniones de tipos

En versiones modernas de Python, una anotación puede contener una unión:

@describir.register
def _(valor: int | float):
    return f"Número: {valor}"

La misma implementación atenderá enteros y números de punto flotante. Conviene mantener las uniones pequeñas. Si los tipos necesitan reglas diferentes, dos registros separados serán más claros.

Solo el primer argumento controla el despacho

Esta es la limitación principal. En una función como convertir(valor, formato), solamente valor selecciona la implementación. El segundo parámetro puede modificar la lógica interna, pero no participa en el despacho.

@singledispatch
def convertir(valor, formato="texto"):
    raise TypeError(
        f"Tipo no compatible: {type(valor).__name__}"
    )

@convertir.register
def _(valor: int, formato="texto"):
    if formato == "hex":
        return hex(valor)
    return str(valor)

Si el comportamiento depende de la combinación de dos tipos, considera clases, un registro de estrategias, condiciones explícitas o una biblioteca de despacho múltiple.

Diseñar una implementación predeterminada segura

La función base debe tener un propósito claro. Para una representación visual, un texto genérico puede ser suficiente. En serialización o validación, aceptar silenciosamente un tipo desconocido puede ser peligroso. En esos casos, es mejor producir un error preciso:

@singledispatch
def crear_payload(valor):
    raise TypeError(
        f"No existe conversor para {type(valor).__name__}"
    )

Un fallo temprano y descriptivo evita que datos inválidos lleguen a otra capa del sistema.

Inspeccionar el registro

La función genérica incluye herramientas útiles. El método dispatch devuelve la implementación que se usaría para un tipo:

implementacion = describir.dispatch(int)
print(implementacion(25))

El atributo de solo lectura registry muestra las asociaciones entre tipos y funciones:

for tipo, funcion in describir.registry.items():
    print(tipo, funcion)

Estas herramientas ayudan a depurar, documentar y probar sistemas donde varias clases o extensiones registran comportamientos.

Cómo probar funciones con singledispatch

Las pruebas deberían cubrir la implementación predeterminada, las especializaciones principales y los casos de herencia:

def test_describir_entero():
    assert describir(5) == "Número: 5"

def test_describir_producto():
    producto = Producto("Ratón", 40.0)
    assert "Ratón" in describir(producto)

def test_describir_fallback():
    resultado = describir(object())
    assert "object" in resultado

Cuando la prioridad entre clases es importante, prueba directamente describir.dispatch(MiTipo). Así detectarás si un registro nuevo altera la resolución esperada.

singledispatchmethod en clases

functools.singledispatchmethod aplica la misma idea a métodos de instancia o de clase:

from functools import singledispatchmethod

class Exportador:
    @singledispatchmethod
    def exportar(self, valor):
        raise TypeError("Tipo no compatible")

    @exportar.register
    def _(self, valor: str):
        return {"texto": valor}

    @exportar.register
    def _(self, valor: int):
        return {"numero": valor}

exportador = Exportador()
print(exportador.exportar("Academify"))

El mecanismo examina el primer argumento que no sea self o cls. Es una opción práctica para servicios que convierten, validan, renderizan o exportan distintos tipos.

Extender una función desde otro módulo

Un módulo puede importar la función genérica y registrar un nuevo tipo:

from app.formatos import describir
from app.modelos import Factura

@describir.register
def _(valor: Factura):
    return f"Factura {valor.numero}"

Esta capacidad permite crear extensiones y plugins. Aun así, los registros ejecutados como efecto secundario de una importación pueden resultar difíciles de rastrear. Carga los módulos de extensión en un lugar explícito y documenta qué tipos añaden.

Cuándo singledispatch es una buena elección

  • Una operación cambia claramente según el tipo del dato.
  • Quieres mantener una sola función pública.
  • Nuevos tipos deben añadirse sin editar una cadena central de condiciones.
  • La herencia o las interfaces abstractas ya expresan compatibilidad.
  • Cada implementación puede ser pequeña y probarse por separado.

Cuándo conviene evitarlo

No uses singledispatch únicamente para eliminar cualquier if. Una condición corta puede ser más fácil de leer. Evítalo cuando el comportamiento depende de valores y no de tipos, cuando varios argumentos determinan el resultado, cuando existen solamente dos casos simples y estables o cuando los métodos polimórficos de las propias clases expresan mejor la responsabilidad.

Tampoco es recomendable dispersar registros por módulos sin una política clara. Un registro global oculto puede hacer que el comportamiento cambie según el orden de importación.

Errores frecuentes

  • Anotar el parámetro equivocado: el primer argumento de despacho debe contener el tipo correcto.
  • Esperar despacho por el segundo argumento: solo uno participa en la selección.
  • Registrar clases demasiado concretas: una clase base abstracta puede aceptar más entradas válidas.
  • Usar un fallback inseguro: los tipos desconocidos deben generar un valor válido o un error claro.
  • Confundir anotaciones con validación: el registro usa el tipo, pero no valida automáticamente los atributos internos.

Conclusión

singledispatch en Python organiza comportamientos específicos por tipo detrás de una sola función genérica. Puede sustituir cadenas extensas de isinstance, conservar una API estable y aprovechar la jerarquía de clases para encontrar la implementación más específica.

Utilízalo cuando el dominio realmente varía según el tipo. Mantén un fallback explícito, registros fáciles de localizar, abstracciones adecuadas y pruebas que comprueben tanto el resultado como la selección. Con estas prácticas, singledispatch aporta extensibilidad sin convertir el comportamiento de la función en una complejidad oculta.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Código Python y atributos
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Descriptors en Python: guía práctica

    Aprende descriptors en Python con __get__, __set__, validación, property, almacenamiento por instancia, pruebas, herencia y buenas prácticas.

    Ler mais

    Tempo de leitura: 7 minutos
    22/07/2026
    Leitura de arquivos grandes em Python sem travar o sistema
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Cómo leer archivos gigantes con Python sin bloquear el sistema

    Aprende a leer archivos gigantes con Python usando iteración, bloques, generadores, chunks de Pandas, compresión y puntos de control.

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026
    Herança múltipla em Python sem causar problemas no código
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Herencia múltiple en Python: MRO, super() y mixins

    Aprende herencia múltiple en Python con MRO, super(), mixins, problema del diamante, inicializadores cooperativos, composición y pruebas.

    Ler mais

    Tempo de leitura: 4 minutos
    11/07/2026
    Criando instalador EXE com ícone personalizado em Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Cómo crear un ejecutable EXE con icono personalizado en Python

    Crea un EXE de Python con PyInstaller, icono ICO, onefile, windowed, archivos de datos, SPEC, rutas seguras, pruebas y distribución.

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026
    Uso do super em Python para resolver problemas de herança
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Cómo usar super() en Python sin errores de herencia

    Aprende a usar super() en Python con __init__, MRO, herencia múltiple, mixins, argumentos cooperativos, pruebas y composición.

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026
    Uso de multiprocessing em Python para acelerar scripts
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Multiprocessing en Python: usa varios núcleos correctamente

    Aprende multiprocessing en Python con Process, Pool, Queue, memoria compartida, ProcessPoolExecutor, benchmarks y buenas prácticas.

    Ler mais

    Tempo de leitura: 4 minutos
    11/07/2026