El módulo contextvars mantiene valores locales al contexto de ejecución. Resuelve un problema común en aplicaciones concurrentes: disponer de ID de solicitud, usuario, tenant, locale o metadatos de tracing sin pasar argumentos por todas las funciones y sin permitir que el estado de una tarea se filtre a otra.
En código síncrono con threads, threading.local() puede servir. En asyncio, muchas tareas comparten la misma thread y alternan su ejecución, por lo que ContextVar sigue a la tarea lógica.
Declarar una ContextVar
Crea variables a nivel de módulo, no dentro de closures. Los objetos Context mantienen referencias fuertes y una creación dinámica puede impedir la recolección.
from contextvars import ContextVar
request_id: ContextVar[str] = ContextVar("request_id")
usuario_actual: ContextVar[str | None] = ContextVar(
"usuario_actual", default=None
)El nombre sirve para depuración. El default opcional debe ser coherente con el tipo esperado.
Leer el valor actual
get() busca en el contexto actual. Devuelve primero el default del método, después el definido al crear la variable y, si no existe ninguno, genera LookupError.
print(usuario_actual.get())
try:
print(request_id.get())
except LookupError:
print("request_id no definido")Para metadatos obligatorios, omitir el default puede revelar errores de integración. Para datos realmente opcionales, un default explícito simplifica el uso.
Definir y restaurar con token
set() cambia el valor en el contexto actual y devuelve un Token que restaura exactamente el estado anterior.
token = request_id.set("req-123")
try:
procesar()
finally:
request_id.reset(token)El mismo token no puede usarse dos veces y pertenece a la variable que lo creó.
Tokens como context managers en Python 3.14
Python 3.14 permite usar el token devuelto por set() como context manager.
with request_id.set("req-456"):
print(request_id.get())
# valor anterior restauradoPara compatibilidad con versiones anteriores, usa set(), try/finally y reset().
Aislamiento en asyncio
Cuando se crea una task, se copia su contexto actual. Los cambios posteriores dentro de una tarea quedan aislados.
import asyncio
from contextvars import ContextVar
nombre = ContextVar("nombre")
async def trabajo(valor):
with nombre.set(valor):
await asyncio.sleep(0.01)
return nombre.get()
async def main():
resultado = await asyncio.gather(trabajo("A"), trabajo("B"))
print(resultado)
asyncio.run(main())Incluso después de un await, cada tarea lee su propio valor.
No es una global normal
El objeto ContextVar es global, pero el valor depende del contexto. Copiarlo también a una variable global común destruye el aislamiento.
No uses contexto para ocultar datos de dominio que deberían ser argumentos. Es más apropiado para metadatos pequeños y transversales.
ID de solicitud en logs
request_id = ContextVar("request_id", default="-")
def log(mensaje):
print(f"[{request_id.get()}] {mensaje}")
async def tratar_solicitud(identificador):
with request_id.set(identificador):
log("inicio")
await llamar_servicio()
log("fin")Los frameworks de logging pueden usar filtros o adapters que consulten la variable. Evita añadir secretos o datos personales innecesarios.
Tenant y usuario
Una aplicación multitenant puede exponer el tenant actual por contexto, pero cada consulta debe aplicar filtros y autorización. La propagación contextual no es una barrera de seguridad.
Define el valor en el borde de la solicitud, valida el principal y restáuralo al finalizar.
copy_context
copy_context() copia el contexto actual en tiempo O(1), independientemente del número de variables.
from contextvars import copy_context
ctx = copy_context()
for variable, valor in ctx.items():
print(variable.name, valor)La copia puede ejecutar código con ctx.run(funcion, *args). Los cambios permanecen en ese objeto Context, no en el contexto exterior.
Ejecutar en un contexto específico
ctx = copy_context()
def tarea():
request_id.set("aislado")
return request_id.get()
resultado = ctx.run(tarea)El mismo contexto no puede entrar simultáneamente más de una vez, incluso desde otra thread. Esto genera RuntimeError. Tras salir, puede reutilizarse.
Propagación a threads
Cada thread tiene su propia pila efectiva de contextos. La propagación a un executor depende de la API. Para un comportamiento explícito, captura el contexto y ejecuta dentro de él.
ctx = copy_context()
futuro = executor.submit(ctx.run, funcion)No envíes el mismo contexto de forma concurrente a varios workers. Crea copias separadas.
ContextVar frente a threading.local
threading.local() aísla por thread física. Cientos de tareas asyncio pueden vivir en una sola thread, por lo que compartirían el estado local. ContextVar sigue el contexto lógico.
Defaults mutables
Evita defaults mutables compartidos, como listas y diccionarios. El aislamiento se aplica a la referencia, no a mutaciones del mismo objeto.
# evitar
errores = ContextVar("errores", default=[])
# crear un valor por ámbito
with errores.set([]):
errores.get().append("fallo")Valores inmutables, IDs y dataclasses congeladas reducen sorpresas.
Tokens anidados y orden
Las definiciones pueden anidarse. Restaura en orden inverso.
t1 = request_id.set("externo")
t2 = request_id.set("interno")
request_id.reset(t2)
request_id.reset(t1)Usar el token equivocado o reutilizarlo genera error. La sintaxis de context manager de Python 3.14 hace el anidamiento más visible.
Callbacks y planificación
Los callbacks suelen ejecutarse en el contexto capturado por la API que los planificó, pero un framework puede definir reglas distintas. Prueba con el event loop, executors y callbacks reales.
Probar aislamiento
async def probar_aislamiento():
async def leer(valor):
with request_id.set(valor):
await asyncio.sleep(0)
return request_id.get()
a, b = await asyncio.gather(leer("A"), leer("B"))
assert (a, b) == ("A", "B")Prueba también excepciones, cancelación, subtasks y ejecución en thread. Confirma que el valor anterior se restaura.
No crear un contenedor oculto
Guardar clientes de base de datos, HTTP y servicios completos en contexto oculta dependencias y complica tests. Pasa dependencias principales explícitamente y reserva el contexto para metadatos pequeños.
Errores frecuentes
- Crear ContextVars dentro de closures.
- Olvidar restaurar el token.
- Usar un default mutable compartido.
- Tratar el contexto como autorización.
- Esperar que thread-local aísle tareas asyncio.
- Entrar simultáneamente en el mismo Context.
- Ocultar dependencias importantes en contexto.
Buenas prácticas
- Declara ContextVars a nivel de módulo.
- Usa nombres útiles.
- Restaura valores con tokens o context managers.
- Prefiere datos pequeños e inmutables.
- Captura contexto explícitamente al cambiar de thread.
- Prueba cancelación y concurrencia.
- Mantén autorización fuera del contexto.
Guías relacionadas
Continúa con ExitStack en Python, inspect en Python, faulthandler en Python, traceback en Python y operator en Python.
Consulta la documentación oficial de contextvars y la PEP 567.
Conclusión
contextvars ofrece estado contextual aislado para código síncrono y asíncrono. Es ideal para IDs de solicitud, tracing, locale y pequeños metadatos transversales. Su uso seguro exige ámbitos claros, restauración garantizada, defaults inmutables y pruebas entre tareas y threads.







