Cuando escribes un decorador, reemplazas la función original con otra función —el wrapper— que la envuelve. El problema es que ese wrapper tiene su propio __name__, su propio __doc__, sus propias __annotations__: los atributos que identifican a una función. Sin intervención explícita, la función original queda borrada del mapa desde el punto de vista de cualquier herramienta que inspeccione el código: loggers, frameworks web, debuggers, help(), pytest, Sphinx.
@wraps(func), de functools, copia esos atributos al wrapper con una sola línea. Su base es functools.update_wrapper, una función que recibe (wrapper, wrapped) y transfiere __name__, __qualname__, __doc__, __annotations__, __module__ y __dict__, y además guarda una referencia a la función original en wrapper.__wrapped__. @wraps es simplemente update_wrapper empaquetado como decorador para que sea cómodo de usar encima de la definición del wrapper.
La razón de ser de este mecanismo es filosófica y práctica a la vez: un decorador bien escrito debería ser transparente. El código que llama a tu función decorada no debería saber —ni importarle— que hay un wrapper de por medio. Cuando un framework de logging registra func.__name__, espera ver "procesar_pago", no "wrapper". Cuando alguien llama a help(mi_funcion) en una sesión interactiva, merece ver la docstring real.
El momento para usarlo es siempre: cada vez que un decorador define una función interna que sustituye a la original, @wraps va ahí. No hay un caso válido en el que quieras que el wrapper se haga pasar por sí mismo.
Lo que se rompe si lo omites es sutil en desarrollo y brutal en producción. Los stacktraces mencionan wrapper en lugar del nombre real. Los sistemas de métricas agrupan todas las funciones decoradas bajo el mismo nombre. Las herramientas que leen __annotations__ para validar tipos —Pydantic, FastAPI, typer— pueden fallar silenciosamente o lanzar errores crípticos porque las anotaciones del wrapper son distintas a las de la función original.
import functools
import time
import logging
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
def timed(func):
"""Decorador que mide el tiempo de ejecución de una función."""
@functools.wraps(func) # copia __name__, __doc__, __annotations__, etc.
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
# __name__ aquí ya refleja la función original, no "wrapper"
logger.info("%s tardó %.4f segundos", func.__name__, elapsed)
return result
return wrapper
@timed
def fetch_user_data(user_id: int, include_orders: bool = False) -> dict:
"""Recupera los datos de un usuario desde la base de datos.
Args:
user_id: Identificador único del usuario.
include_orders: Si es True, incluye el historial de pedidos.
"""
time.sleep(0.05) # simula latencia de red
return {"id": user_id, "name": "Ada Lovelace"}
if __name__ == "__main__":
fetch_user_data(42, include_orders=True)
# Comprobaciones de introspección
print(fetch_user_data.__name__) # → fetch_user_data
print(fetch_user_data.__doc__[:45]) # → Recupera los datos de un usuario...
print(fetch_user_data.__annotations__) # → {'user_id': <class 'int'>, ...}
print(fetch_user_data.__wrapped__) # → la función original sin decorar
Lo que está pasando capa por capa
El decorador timed devuelve wrapper, no fetch_user_data. Sin @functools.wraps(func), después de la decoración fetch_user_data.__name__ sería "wrapper" y fetch_user_data.__doc__ sería None: el wrapper no tiene docstring propio.
Al poner @functools.wraps(func) encima de def wrapper(...), update_wrapper se ejecuta justo cuando timed construye el wrapper. En ese momento copia los atributos de func —la función que se está decorando— al objeto wrapper que se está creando. El resultado es que wrapper adquiere la identidad de fetch_user_data antes de que timed lo devuelva.
El atributo __wrapped__ que update_wrapper añade es especialmente valioso: le permite a inspect.unwrap() recorrer la cadena de decoradores hasta llegar a la función original. Frameworks como unittest.mock lo usan para parchear la función correcta, y algunas herramientas de introspección lo usan para obtener la firma real.
Fíjate en que dentro de wrapper usamos func.__name__ en el log, no wrapper.__name__. Ambos darían el mismo resultado gracias a @wraps, pero usar func.__name__ es más explícito sobre la intención: queremos el nombre de lo que envolvemos.
Las __annotations__ son especialmente críticas. FastAPI, por ejemplo, lee las anotaciones de las funciones de ruta para construir el esquema OpenAPI y validar los parámetros de entrada. Si @wraps no estuviera ahí, FastAPI vería las anotaciones del wrapper —(*args, **kwargs), sin anotar— y o bien fallaría o bien generaría documentación vacía.
Errores que debes conocer
Error: Omitir @wraps en decoradores con estado que guardan el wrapper en una clase, asumiendo que el __init__ preserva la identidad.
# ❌ Wrong
class retry:
def __init__(self, func):
self.func = func # __name__ y __doc__ no se propagan al objeto
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
@retry
def connect(host: str) -> bool:
"""Establece conexión con el host."""
...
print(connect.__name__) # → 'retry', no 'connect'
# ✅ Right
class retry:
def __init__(self, func):
self.func = func
functools.update_wrapper(self, func) # propaga atributos al objeto callable
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
@retry
def connect(host: str) -> bool:
"""Establece conexión con el host."""
...
print(connect.__name__) # → 'connect'
update_wrapper funciona igual sobre objetos callable que sobre funciones; copia los atributos directamente al objeto self.
Error: Decoradores anidados donde solo el exterior usa @wraps, dejando al wrapper intermedio sin él.
# ❌ Wrong
def logged(func):
def wrapper(*args, **kwargs): # este wrapper interno no tiene @wraps
logger.info("llamando a algo...")
return func(*args, **kwargs)
return wrapper
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@timed
@logged # logged se aplica primero; su wrapper no tiene @wraps
def process(): ...
print(process.__name__) # → 'wrapper' — timed copió el nombre del wrapper de logged
# ✅ Right
def logged(func):
@functools.wraps(func) # cada nivel de decorador preserva la identidad
def wrapper(*args, **kwargs):
logger.info("llamando a %s...", func.__name__)
return func(*args, **kwargs)
return wrapper
Cada decorador en la pila recibe como func el resultado del decorador anterior, así que si alguno en la cadena no usa @wraps, la identidad se pierde desde ese punto hacia arriba.
N° 117