Em servidores assíncronos, várias requisições compartilham uma thread. Uma variável global comum pode misturar o identificador de uma requisição com outra. ContextVar mantém valores locais ao contexto e acompanha tarefas do asyncio.

Definir, usar e restaurar

from contextvars import ContextVar

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


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

Declare a variável no nível do módulo. Guarde o token retornado por set() e use reset() em finally, inclusive quando uma etapa gera exceção. Isso torna o ciclo de vida visível.

ContextVar é apropriada para request ID, trace ID e contexto de logging. Dados essenciais à regra de negócio devem continuar como parâmetros explícitos. O post sobre logs estruturados com structlog mostra uma aplicação de correlação, e asyncio.TaskGroup explica concorrência estruturada.

Entender fronteiras

Novas tarefas normalmente recebem uma cópia do contexto atual, mas threads, processos e sistemas de filas são outras fronteiras. Propague identificadores de forma intencional e não presuma que uma variável atravessa a rede. Em testes, configure e restaure o contexto para evitar dependência da ordem de execução.

Por que uma variável global falha

Imagine duas requisições que atribuem uma variável global chamada request_id_atual. A primeira escreve req-a e suspende em um await. A segunda escreve req-b. Quando a primeira retoma, encontra req-b. O problema não exige duas threads: a alternância cooperativa no mesmo loop de eventos basta.

Uma variável local evita a colisão, mas funções profundas só conseguem lê-la se todas as camadas intermediárias receberem e repassarem o parâmetro. ContextVar atende ao caso mais estreito dos metadados transversais de execução. A declaração fica no módulo, enquanto cada contexto conserva seu valor. Logging, tracing e métricas podem consultá-lo sem adicionar detalhes de infraestrutura às funções de domínio.

Não esconda dados de negócio por conveniência. Cliente, permissões e total de um pedido devem continuar explícitos quando determinam comportamento. Um identificador de correlação usado somente para observar a execução é um candidato melhor.

Propagação entre tarefas

Uma tarefa criada com asyncio.create_task() captura o contexto vigente no momento da criação. Alterações posteriores no pai não reescrevem o contexto já capturado pelo filho. Cada tarefa também pode mudar seu valor sem modificar as demais.

import asyncio
from contextvars import ContextVar

correlacao = ContextVar("correlacao", default="sem-id")


async def filho() -> str:
    await asyncio.sleep(0)
    return correlacao.get()


async def main() -> None:
    token = correlacao.set("pedido-42")
    try:
        tarefa = asyncio.create_task(filho())
        correlacao.set("outro-valor")
        print(await tarefa)  # pedido-42
    finally:
        correlacao.reset(token)


asyncio.run(main())

Esse comportamento permite iniciar tarefas relacionadas com os mesmos metadados. Se uma tarefa precisar começar com contexto limpo ou preparado de outra forma, torne essa política explícita. APIs atuais de tarefas aceitam um context específico, e copy_context() permite capturar um contexto e executar código na cópia.

O token preserva o valor anterior

O retorno de set() não é o novo valor. É um token que registra o estado necessário para desfazer a atribuição. Em escopos aninhados, uma função interna pode sobrescrever temporariamente o identificador e depois revelar novamente o valor externo.

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

Não use set(None) como substituto genérico de reset(). Se havia um valor anterior, atribuir None perde essa relação. O token pertence à variável e ao contexto que o produziram; restaure-o em ordem coerente com os escopos.

Integrar com logs com segurança

Uma arquitetura prática gerencia o contexto no limite da aplicação. O handler HTTP valida ou gera o identificador, define o valor antes de chamar a aplicação e restaura o token ao terminar. Um processador de log inclui o campo atual em cada evento. Serviços de domínio permanecem independentes da implementação do logger.

Trate identificadores recebidos como entrada não confiável. Limite tamanho e caracteres, ou gere seu próprio valor. Caso contrário, um cliente pode injetar texto confuso ou enorme em cada linha do log. Isolamento não é sigilo: não guarde credenciais, tokens de sessão ou dados pessoais desnecessários apenas porque o valor é local à tarefa.

Threads, executores e processos

Cada thread possui sua pilha de contextos. Ao delegar trabalho síncrono, confira o contrato da API. asyncio.to_thread() propaga o contexto atual para a função, enquanto integrações de executor de nível inferior podem exigir copy_context().run. Processos separados não compartilham esse valor em memória; os metadados precisam viajar na mensagem ou no protocolo.

Em chamadas HTTP, encaminhe somente campos previstos pela política de correlação. No consumidor de uma fila, extraia os metadados, defina o contexto durante uma mensagem e restaure-o em finally. Sem limpeza, um worker persistente pode rotular a mensagem seguinte com dados antigos.

Testar isolamento e limpeza

Um teste útil inicia duas tarefas com identificadores diferentes, força alternância com await asyncio.sleep(0) e confirma que cada uma conserva seu valor. Outro provoca exceção dentro do escopo e verifica que o valor anterior retornou. Testes sequenciais do caminho feliz geralmente não revelam essas falhas.

Uma fixture pode chamar set(), ceder o controle ao teste e executar reset() na finalização. Não dependa de valor deixado por outro teste. Se a aplicação iniciar tarefas em segundo plano, aguarde ou cancele essas tarefas antes de desmontar recursos compartilhados.

Erros comuns e critério de escolha

Criar uma ContextVar repetidamente dentro de uma função complica sua propriedade e pode manter referências em contextos. Declare-a uma vez no módulo. Chamar get() sem padrão pode gerar LookupError; isso é útil quando ausência significa erro de programação, enquanto um padrão explícito serve para metadados opcionais.

Escolha ContextVar para dados transversais cuja duração seja o contexto de execução atual. Use parâmetros para valores do contrato da função, armazenamento persistente para estado que precisa sobreviver à requisição e campos de mensagens para atravessar serviços. Associar mecanismo, duração e fronteira evita transformar contexto local em variável global disfarçada.

A documentação oficial de contextvars, consultada em 22 de julho de 2026, descreve ContextVar, tokens e cópia de contexto. A ferramenta resolve isolamento contextual, não gerenciamento geral de estado.

Checklist de revisão

Antes de publicar a integração, confirme onde o valor nasce, quais funções podem alterá-lo e em qual bloco ele é restaurado. Verifique se toda chamada a set() possui um reset() correspondente em finally. Liste também as fronteiras externas: threads, processos, filas e requisições de rede exigem decisões próprias de propagação.

Observe uma execução concorrente em ambiente de teste. Os logs de duas requisições intercaladas devem conservar identificadores distintos, inclusive quando ocorre exceção ou cancelamento. Confirme que o valor não permanece após a conclusão e que entradas recebidas têm limite de tamanho.

Por fim, revise cada leitura da variável. Se uma função não consegue cumprir seu contrato sem aquele valor, talvez o dado deva ser parâmetro obrigatório. Se a ausência é aceitável, um padrão explícito torna o comportamento previsível. Essa revisão mantém ContextVar restrita à observabilidade e a outros interesses transversais, em vez de criar dependências ocultas entre camadas.