Protocol en Python: tipado estructural

Publicado el: 28/08/2026
Tempo de leitura: 4 minutos
A developer typing code on a laptop with a Python book beside in an office.

En proyectos Python grandes, muchas funciones necesitan aceptar objetos con cierto comportamiento sin obligar a que todas las implementaciones hereden de la misma clase. Un servicio puede requerir solamente save(); un logger, write(); un caché, get() y set(). typing.Protocol describe esas capacidades mediante tipado estructural: un objeto es compatible cuando posee los miembros requeridos, aunque no exista herencia explícita.

Esta guía explica cómo definir protocolos, declarar atributos y métodos, crear protocolos genéricos, usar runtime_checkable, modelar callbacks, probar dependencias y decidir cuándo una clase abstracta sigue siendo la mejor opción.

Tipado nominal y tipado estructural

El tipado nominal basa la compatibilidad en nombres y herencia. Una clase implementa una interfaz porque hereda de ella. El tipado estructural se basa en la forma: los métodos y atributos disponibles.

from typing import Protocol

class Guardable(Protocol):
    def guardar(self, destino: str) -> None:
        ...

class Informe:
    def guardar(self, destino: str) -> None:
        print(f"Guardando en {destino}")

def persistir(item: Guardable) -> None:
    item.guardar("salida.txt")

persistir(Informe())

Informe no hereda de Guardable, pero mypy o Pyright puede comprobar que su método coincide con el contrato.

Por qué Protocol reduce acoplamiento

Un protocolo permite que el consumidor declare solamente el comportamiento necesario. La lógica de negocio puede depender de un contrato pequeño para caché, repositorio, correo o reloj, en lugar de depender de un SDK concreto.

El enfoque complementa la guía de type hints en Python. Las anotaciones hacen visibles los contratos; Protocol los mantiene pequeños y orientados al uso.

Protocolos con atributos

Los protocolos pueden exigir atributos, propiedades y métodos.

class UsuarioVisible(Protocol):
    id: int
    nombre: str

    @property
    def activo(self) -> bool:
        ...

def mostrar(usuario: UsuarioVisible) -> str:
    estado = "activo" if usuario.activo else "inactivo"
    return f"{usuario.id}: {usuario.nombre} ({estado})"

Un atributo normal suele implicar lectura y escritura. Si el consumidor solo necesita leer, una propiedad de solo lectura comunica mejor el requisito y evita problemas de varianza.

Firmas precisas

Los tipos de parámetros, los tipos de retorno y la forma de llamada forman parte del contrato. Una implementación que devuelve str no satisface un método que promete bytes. Tampoco puede aceptar entradas más restringidas que las permitidas por el protocolo.

class Serializador(Protocol):
    def dumps(self, valor: object, *, indentar: bool = False) -> str:
        ...

Los parámetros keyword-only, posicionales, opcionales y variádicos deben reflejar el uso real.

Protocolos genéricos

Usa parámetros genéricos cuando el tipo procesado debe conservarse.

from typing import Protocol, TypeVar

T = TypeVar("T")

class Repositorio(Protocol[T]):
    def obtener(self, item_id: int) -> T | None:
        ...

    def agregar(self, item: T) -> None:
        ...

class Producto:
    def __init__(self, nombre: str) -> None:
        self.nombre = nombre

def cargar_producto(repo: Repositorio[Producto], item_id: int) -> Producto:
    producto = repo.obtener(item_id)
    if producto is None:
        raise LookupError(item_id)
    return producto

El analizador conserva la relación entre el repositorio y el tipo retornado, reduciendo casts inseguros.

Protocolos para callbacks

Callable es suficiente para funciones simples, pero un protocolo puede modelar parámetros nombrados, overloads y objetos invocables con atributos.

class AlCompletar(Protocol):
    def __call__(self, resultado: str, *, duracion: float) -> None:
        ...

def ejecutar(callback: AlCompletar) -> None:
    callback("ok", duracion=0.42)

Cualquier función u objeto invocable con una firma compatible satisface el protocolo.

runtime_checkable

Los protocolos están pensados principalmente para análisis estático. Con @runtime_checkable es posible realizar verificaciones superficiales con isinstance().

from typing import Protocol, runtime_checkable

@runtime_checkable
class Cerrable(Protocol):
    def close(self) -> None:
        ...

if isinstance(recurso, Cerrable):
    recurso.close()

La comprobación solo verifica que existan los atributos requeridos. No valida firmas completas, retornos, efectos o semántica. No debe usarse como validación total de una interfaz.

Protocol frente a ABC

Una clase abstracta resulta útil cuando controlas las implementaciones, compartes código, registras subclases o deseas impedir instancias incompletas. Protocol suele ser mejor para aceptar objetos de terceros y aplicar duck typing sin herencia.

La guía sobre clases abstractas con abc explica los contratos nominales. Ambos mecanismos pueden coexistir: una biblioteca puede ofrecer una ABC oficial y un protocolo menor para consumidores.

Inyección de dependencias y pruebas

Los protocolos facilitan dobles de prueba.

class EnviadorCorreo(Protocol):
    def enviar(self, destino: str, asunto: str, cuerpo: str) -> None:
        ...

class ServicioRegistro:
    def __init__(self, correo: EnviadorCorreo) -> None:
        self.correo = correo

    def registrar(self, direccion: str) -> None:
        self.correo.enviar(direccion, "Bienvenido", "Cuenta creada")

class CorreoFake:
    def __init__(self) -> None:
        self.mensajes: list[tuple[str, str, str]] = []

    def enviar(self, destino: str, asunto: str, cuerpo: str) -> None:
        self.mensajes.append((destino, asunto, cuerpo))

CorreoFake no hereda del código de producción. La compatibilidad proviene de la firma del método.

Protocolos recursivos y composición

Un protocolo puede referenciarse a sí mismo y heredar de otros protocolos.

class Nombrado(Protocol):
    nombre: str

class NodoArbol(Nombrado, Protocol):
    @property
    def hijos(self) -> list["NodoArbol"]:
        ...

Es preferible componer contratos pequeños. Un protocolo enorme vuelve a introducir el acoplamiento que el tipado estructural pretende reducir.

Errores comunes

  • Exigir demasiados miembros: incluye solo lo que utiliza el consumidor.
  • Tratar runtime_checkable como validación completa: no comprueba firmas detalladas.
  • Usar atributos mutables cuando solo se necesita lectura: puede generar incompatibilidades.
  • No ejecutar un analizador estático: Protocol no aplica las anotaciones por sí solo en runtime.
  • Crear abstracciones sin necesidad de sustitución: una interfaz debe resolver un problema real.
  • Forzar herencia explícita: elimina la ventaja estructural.

Buenas prácticas

Nombra los protocolos por capacidad, como Legible, Cerrable o Repositorio. Define el contrato cerca de la capa consumidora, porque esa capa conoce el mínimo necesario. Ejecuta mypy o Pyright en CI y agrega pruebas de comportamiento para requisitos que los tipos no pueden expresar.

En APIs públicas, documenta también idempotencia, orden, thread safety, propiedad de recursos y manejo de errores.

Ejemplo completo: caché reemplazable

from typing import Protocol, TypeVar

T = TypeVar("T")

class Cache(Protocol[T]):
    def get(self, clave: str) -> T | None:
        ...

    def set(self, clave: str, valor: T, ttl: int) -> None:
        ...

class CacheMemoria:
    def __init__(self) -> None:
        self._datos: dict[str, object] = {}

    def get(self, clave: str):
        return self._datos.get(clave)

    def set(self, clave: str, valor: object, ttl: int) -> None:
        self._datos[clave] = valor

def cargar(cache: Cache[str], clave: str) -> str:
    valor = cache.get(clave)
    if valor is None:
        valor = "calculado"
        cache.set(clave, valor, ttl=60)
    return valor

Un adaptador Redis, un fake o una biblioteca externa puede satisfacer el mismo contrato sin depender de CacheMemoria.

Conclusión

typing.Protocol añade verificación estática al duck typing tradicional de Python. Permite implementaciones independientes, contratos pequeños propiedad del consumidor, relaciones genéricas, callbacks y pruebas con bajo acoplamiento.

La documentación oficial de Protocol en Python cubre herencia, genéricos y comprobación en runtime. Úsalo cuando el comportamiento importe más que una jerarquía compartida y mantén cada contrato mínimo y preciso.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    A close-up shot showcasing the intricate scales of a snake, highlighting texture and color.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    memoryview en Python: buffers sin copia

    Aprende memoryview en Python para buffers sin copia, slices, bytearray editable, cast, mmap, struct, sockets y control seguro del ciclo

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    tracemalloc en Python: rastrea memoria

    Aprende tracemalloc en Python para medir picos, crear y comparar snapshots, filtrar asignaciones y diagnosticar crecimiento de memoria.

    Ler mais

    Tempo de leitura: 6 minutos
    28/08/2026
    Detailed shot of a Jungle Carpet Python (Morelia spilota cheynei) in its natural habitat.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    queue en Python: comunica threads

    Aprende queue en Python para comunicar threads con FIFO, LIFO, prioridad, backpressure, task tracking, sentinelas y shutdown seguro.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Flat lay of a complete toolset neatly organized in a workshop setting, essential for auto repair tasks.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    selectors en Python: varios sockets

    Aprende selectors en Python para multiplexar sockets, gestionar lecturas y escrituras parciales, buffers, timeouts, wakeup y backpressure.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Detailed close-up texture of a snake's patterned skin showcasing natural patterns and scales.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    contextvars en Python: contexto async

    Aprende contextvars en Python para contexto por task, request IDs, logging, copy_context, propagación a threads y restauración con tokens.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Person writing appointments on a calendar with a blue pen. High angle view.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    sched en Python: programa eventos

    Aprende sched en Python para programar eventos, usar prioridades, cancelar tareas, crear recurrencia sin drift e integrar executors.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026