Los servidores asíncronos atienden varias peticiones en un hilo. Una variable global común puede mezclar identificadores. ContextVar mantiene valores locales al contexto y funciona con tareas de asyncio.

Definir y restaurar

from contextvars import ContextVar

request_id: ContextVar[str | None] = ContextVar("request_id", default=None)


async def procesar(valor: str) -> None:
    token = request_id.set(valor)
    try:
        await ejecutar_pasos()
    finally:
        request_id.reset(token)

Declara la variable en el módulo. Conserva el token de set() y usa reset() en finally, incluso ante excepciones.

ContextVar es apropiada para request ID, trace ID y logging. Los datos de negocio deben seguir como parámetros. Consulta logs estructurados con structlog y asyncio.TaskGroup.

Las nuevas tareas suelen recibir el contexto actual, pero hilos, procesos, colas y red son fronteras distintas. Propaga identificadores intencionalmente. En pruebas, restaura siempre el contexto.

Por qué falla una variable global

Imagina dos peticiones que asignan una variable global llamada request_id_actual. La primera escribe req-a y se suspende en un await. La segunda escribe req-b. Cuando la primera continúa, encuentra req-b. No hacen falta dos hilos: la alternancia cooperativa de un solo bucle de eventos basta.

Una variable local evita la colisión, pero el código profundo solo puede leerla si todas las funciones intermedias reciben y reenvían el parámetro. ContextVar sirve para el caso más limitado de los metadatos transversales de ejecución. La declaración queda en el módulo y cada contexto conserva su valor. Logs, trazas y métricas pueden consultarlo sin añadir detalles de infraestructura a cada función de dominio.

No ocultes entradas de negocio por comodidad. Cliente, permisos y total de un pedido deben ser explícitos cuando determinan el comportamiento. Un identificador de correlación usado solamente para observar una operación es más adecuado.

Propagación entre tareas

Una tarea creada con asyncio.create_task() captura el contexto vigente al crearla. Un cambio posterior en el padre no reescribe el contexto del hijo. Cada tarea puede cambiar su propio valor sin modificar a sus hermanas.

import asyncio
from contextvars import ContextVar

correlacion = ContextVar("correlacion", default="sin-id")


async def hijo() -> str:
    await asyncio.sleep(0)
    return correlacion.get()


async def main() -> None:
    token = correlacion.set("pedido-42")
    try:
        tarea = asyncio.create_task(hijo())
        correlacion.set("otro-valor")
        print(await tarea)  # pedido-42
    finally:
        correlacion.reset(token)


asyncio.run(main())

Este comportamiento permite que tareas relacionadas hereden metadatos. Si una tarea debe comenzar con un contexto limpio o preparado de otra forma, expresa esa política. Las API actuales pueden aceptar un context concreto y copy_context() permite capturar un contexto y ejecutar código en la copia.

El token restaura estados anidados

El objeto devuelto por set() no es el valor nuevo. Guarda información para deshacer esa asignación. Una operación interna puede sustituir temporalmente un identificador y después recuperar el valor exterior con su token.

exterior = request_id.set("req-123")
try:
    interior = request_id.set("req-123-etapa")
    try:
        print(request_id.get())
    finally:
        request_id.reset(interior)
finally:
    request_id.reset(exterior)

Asignar None no sustituye a reset(). Si existía un valor anterior, la asignación pierde esa relación en vez de restaurarla. El token pertenece a la variable y al contexto que lo crearon. Restaura en un orden coherente con los ámbitos.

Integrar el contexto con logs

Una integración clara administra el contexto en el límite de la aplicación. El handler HTTP valida o genera el identificador, lo establece antes de invocar la aplicación y restaura el token al terminar. Un procesador de logs añade el campo a cada evento. Los servicios de dominio quedan separados de la implementación del logger.

Considera los identificadores recibidos como entrada no confiable. Limita longitud y caracteres, o genera un valor propio. De lo contrario, un cliente puede introducir texto confuso o enorme en cada línea. El aislamiento no aporta confidencialidad: no guardes credenciales, tokens de sesión ni datos personales innecesarios solo porque sean locales a la tarea.

Hilos, ejecutores y procesos

Cada hilo mantiene su pila de contextos. Al delegar trabajo síncrono, revisa el contrato de la API. asyncio.to_thread() propaga el contexto a la función, mientras que integraciones de executor inferiores pueden requerir copy_context().run. Un proceso separado no comparte este valor en memoria; los metadatos deben viajar en el mensaje o protocolo.

En llamadas HTTP, reenvía solo campos previstos por la política de correlación. En consumidores de colas, extrae metadatos, establece el contexto durante un mensaje y restáuralo en finally. Sin limpieza, un worker persistente puede etiquetar el mensaje siguiente con datos antiguos.

Probar aislamiento y limpieza

Una prueba útil inicia dos tareas con identificadores diferentes, fuerza la alternancia con await asyncio.sleep(0) y confirma que cada una conserva su valor. Otra provoca una excepción dentro del ámbito y comprueba que reaparece el valor anterior. Las pruebas secuenciales del camino feliz no suelen revelar estos defectos.

Una fixture puede llamar a set(), ceder el control a la prueba y ejecutar reset() al finalizar. No dependas del valor dejado por otra prueba. Si la aplicación inicia tareas en segundo plano, espera su finalización o cancélalas antes de desmontar recursos.

Errores frecuentes y criterio de elección

Crear una ContextVar repetidamente dentro de una función complica su propiedad y puede mantener referencias desde contextos. Declárala una vez en el módulo. get() sin valor predeterminado puede lanzar LookupError; resulta útil si la ausencia representa un fallo de programación, mientras que un valor predeterminado sirve para metadatos opcionales.

Elige ContextVar para datos transversales cuya vida sea el contexto actual. Usa parámetros para valores del contrato de una función, almacenamiento persistente para estado que debe sobrevivir a la petición y campos de mensajes para atravesar servicios. Relacionar mecanismo, duración y límite evita convertir el contexto en una variable global disimulada.

La documentación oficial de contextvars, consultada el 22 de julio de 2026, describe variables, tokens y copia de contexto. Resuelve aislamiento contextual, no el estado general de la aplicación.

Lista de revisión

Antes de publicar la integración, identifica dónde nace el valor, qué funciones pueden cambiarlo y qué bloque lo restaura. Cada set() debe tener un reset() correspondiente en finally. Enumera también los límites externos: hilos, procesos, colas y peticiones de red necesitan una política explícita de propagación.

Observa una ejecución concurrente en pruebas. Los logs de dos peticiones intercaladas deben conservar identificadores distintos incluso ante excepciones y cancelaciones. Confirma que el valor desaparece al terminar y que los identificadores recibidos tienen límites estrictos de longitud y formato.

Por último, revisa cada lectura. Si una función no puede cumplir su contrato sin el valor, quizá el dato deba ser un parámetro obligatorio. Si la ausencia es válida, un valor predeterminado explícito documenta el comportamiento. Esta revisión mantiene ContextVar enfocada en observabilidad y otros intereses transversales sin crear dependencias ocultas.