ParamSpec conserva la firma completa de una callable, pero algunos decoradores cambian intencionalmente los parámetros visibles. Pueden inyectar un contexto, ocultar una conexión, añadir un lock o adaptar un handler para un framework. typing.Concatenate describe una transformación al inicio de la lista de parámetros.
Esta guía muestra cómo combinar Concatenate con ParamSpec y TypeVar, crear decoradores de inyección, autenticación y sincronización, trabajar con wrappers async y callbacks, y entender los límites de la herramienta.
El problema de inyectar contexto
from collections.abc import Callable
class Contexto:
usuario: str
def inyectar_contexto(funcion: Callable):
def wrapper(*args, **kwargs):
contexto = Contexto()
contexto.usuario = "sistema"
return funcion(contexto, *args, **kwargs)
return wrapperLa función original requiere Contexto primero, mientras la función decorada debe ocultar ese argumento. Sin tipado preciso se pierden autocompletado y validación.
Concatenate con ParamSpec
from collections.abc import Callable
from typing import Concatenate, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def inyectar_contexto(
funcion: Callable[Concatenate[Contexto, P], R],
) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
contexto = Contexto()
contexto.usuario = "sistema"
return funcion(contexto, *args, **kwargs)
return wrapperConcatenate[Contexto, P] significa que la callable original recibe Contexto y después todos los parámetros capturados por P. El wrapper público expone solo P.
Por qué aparece dentro de Callable
Concatenate está diseñado para listas de parámetros de callables. Normalmente aparece como primer argumento de Callable y termina con un ParamSpec.
Callable[Concatenate[Contexto, P], R]Los tipos explícitos aparecen antes de P. No sirve para insertar parámetros en el medio o al final de la firma capturada.
Inyectar una conexión
class Conexion:
def ejecutar(self, sql: str) -> list[tuple[object, ...]]:
...
def con_conexion(
funcion: Callable[Concatenate[Conexion, P], R],
) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
conexion = abrir_conexion()
try:
return funcion(conexion, *args, **kwargs)
finally:
conexion.cerrar()
return wrapperEl consumidor no proporciona la conexión. El decorador controla creación y cierre, mientras la función de negocio recibe una dependencia tipada explícitamente.
Conservar metadatos
from functools import wraps
@wraps(funcion)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
...wraps conserva nombre, documentación y referencia a la función original en runtime. Concatenate, ParamSpec y TypeVar preservan la relación estática.
Inyectar un lock
from threading import Lock
def con_lock(
funcion: Callable[Concatenate[Lock, P], R],
) -> Callable[P, R]:
lock = Lock()
@wraps(funcion)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
with lock:
return funcion(lock, *args, **kwargs)
return wrapperEl lock se comparte entre llamadas. Documenta granularidad y reentrancia porque la anotación no describe esos detalles.
Inyectar usuario autenticado
class Usuario:
id: int
nombre: str
def requiere_usuario(
funcion: Callable[Concatenate[Usuario, P], R],
) -> Callable[P, R]:
@wraps(funcion)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
usuario = usuario_actual()
if usuario is None:
raise PermissionError("autenticación necesaria")
return funcion(usuario, *args, **kwargs)
return wrapperLa función de negocio declara que necesita Usuario, mientras la entrada pública obtiene esa dependencia del entorno.
Varios parámetros inyectados
def inyectar_servicios(
funcion: Callable[
Concatenate[Usuario, Conexion, P],
R,
],
) -> Callable[P, R]:
@wraps(funcion)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
usuario = obtener_usuario()
conexion = abrir_conexion()
try:
return funcion(usuario, conexion, *args, **kwargs)
finally:
conexion.cerrar()
return wrapperPueden aparecer varios tipos antes de P. Su orden debe coincidir exactamente con la llamada real.
Transformación inversa
Un wrapper también puede añadir un parámetro para el consumidor y ocultarlo de la función interna.
def exigir_token(
funcion: Callable[P, R],
) -> Callable[Concatenate[str, P], R]:
@wraps(funcion)
def wrapper(
token: str,
*args: P.args,
**kwargs: P.kwargs,
) -> R:
validar_token(token)
return funcion(*args, **kwargs)
return wrapperEl consumidor ahora proporciona un token antes de los parámetros originales.
Wrappers asíncronos
from collections.abc import Awaitable
class ContextoAsync:
trace_id: str
def con_trace(
funcion: Callable[
Concatenate[ContextoAsync, P],
Awaitable[R],
],
) -> Callable[P, Awaitable[R]]:
@wraps(funcion)
async def wrapper(
*args: P.args,
**kwargs: P.kwargs,
) -> R:
contexto = ContextoAsync()
contexto.trace_id = crear_trace_id()
return await funcion(contexto, *args, **kwargs)
return wrapperLa relación de parámetros es igual a la versión síncrona y el resultado sigue siendo awaitable.
Concatenate en callbacks
def registrar_handler(
handler: Callable[Concatenate[Evento, P], None],
*args: P.args,
**kwargs: P.kwargs,
) -> None:
evento = esperar_evento()
handler(evento, *args, **kwargs)La función de registro controla Evento y el consumidor proporciona los argumentos restantes compatibles con P.
Métodos y self
En métodos, self o cls ya pertenece a la firma capturada. Un decorador que solo reenvía argumentos suele necesitar únicamente ParamSpec. Concatenate es relevante cuando cambia el inicio visible.
Los descriptores vinculan automáticamente el receptor, por lo que inyectar una dependencia después de self puede ser difícil de expresar con un prefijo genérico. Un Protocol específico o overload explícito puede ser más claro.
Límite: solo prefijos
Concatenate añade tipos antes de P. No inserta un parámetro después del primer elemento capturado ni representa un nuevo keyword-only al final. Transformaciones complejas pueden requerir overloads, Protocols o plugins.
Parámetros nombrados
Los tipos añadidos funcionan como parámetros posicionales iniciales. Nombres, defaults y naturaleza keyword-only pueden no expresarse como deseas. Si los nombres públicos son importantes, define un Protocol con una firma explícita de __call__.
Concatenate frente a overload
Concatenate modela una transformación genérica. Overload modela un conjunto finito de formas alternativas. Usa Concatenate cuando cualquier P se conserva tras añadir o quitar un prefijo; usa overload cuando existen pocas firmas concretas con resultados distintos.
Concatenate frente a Protocol
Protocol es más detallado para callables con nombres específicos, atributos extra o métodos. Concatenate es compacto para decoradores genéricos. Ambos pueden combinarse.
Concatenate frente a functools.partial
functools.partial fija argumentos en runtime. Concatenate describe estáticamente la relación entre callables. Un helper tipado para partials puede usar ParamSpec y Concatenate, aunque la inferencia de keywords complejos puede variar.
Cambiar el retorno
def con_contexto_opcional(
funcion: Callable[Concatenate[Contexto, P], R],
) -> Callable[P, R | None]:
@wraps(funcion)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None:
contexto = intentar_contexto()
if contexto is None:
return None
return funcion(contexto, *args, **kwargs)
return wrapperConcatenate gestiona parámetros; la anotación de retorno debe mostrar cualquier transformación.
Errores comunes
- Usar Concatenate sin ParamSpec final: la construcción queda incompleta.
- Intentar insertar en el medio: solo modela prefijos.
- Cambiar el orden: los tipos deben coincidir con la llamada real.
- Olvidar P.args y P.kwargs: no se conservan los parámetros restantes.
- Ignorar binding de métodos: los descriptores cambian el comportamiento.
- Suponer que el tipado valida runtime: el wrapper debe construir dependencias correctas.
Ejemplo completo: unidad de trabajo
from collections.abc import Callable
from functools import wraps
from typing import Concatenate, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
class UnidadTrabajo:
def __enter__(self):
return self
def confirmar(self) -> None:
...
def __exit__(self, exc_type, exc, tb) -> None:
if exc is not None:
self.revertir()
def revertir(self) -> None:
...
def transaccional(
funcion: Callable[Concatenate[UnidadTrabajo, P], R],
) -> Callable[P, R]:
@wraps(funcion)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
with UnidadTrabajo() as uow:
resultado = funcion(uow, *args, **kwargs)
uow.confirmar()
return resultado
return wrapper
@transaccional
def crear_pedido(
uow: UnidadTrabajo,
cliente_id: int,
items: list[int],
) -> int:
...El consumidor ve únicamente cliente_id e items. La función interna declara su dependencia explícitamente y sigue siendo fácil de probar.
Probar la firma
Usa reveal_type(), fixtures de mypy o pyright y llamadas inválidas. Las pruebas de runtime también deben verificar orden, binding y ciclo de vida.
Conclusión
typing.Concatenate complementa ParamSpec al modelar callables que añaden o eliminan parámetros al comienzo de una firma. Es útil para contexto, autenticación, locks, conexiones y adaptadores.
La documentación oficial de Concatenate en Python define la construcción. Consulta también ParamSpec en Python y usa Protocol u overloads cuando la transformación no pueda expresarse como prefijo genérico.







