Voltar ao Glossario
Glossario Python

Context Manager em Python: O que É e Como Funciona | Python Brasil

Aprenda context managers em Python: with, __enter__, __exit__, contextlib, async, ExitStack, suppress e padrões reais. Guia completo com exemplos.

O que é um Context Manager?

Um context manager é um objeto que define o comportamento de entrada e saída de um bloco with. Ele garante que recursos (como arquivos, conexões de banco de dados ou locks) sejam corretamente liberados após o uso, mesmo se ocorrer uma exceção.

É um dos padrões mais elegantes do Python para gerenciamento seguro de recursos, eliminando erros de “esqueceu de fechar” que ocorrem com código manual de cleanup.

O bloco with: por que ele existe

# Sem context manager — propenso a erros
f = open('dados.txt', 'r')
try:
    conteudo = f.read()
    processar(conteudo)
finally:
    f.close()  # Precisamos do try/finally para garantir o fechamento

# Com context manager — limpo e seguro
with open('dados.txt', 'r') as f:
    conteudo = f.read()
    processar(conteudo)
# Arquivo fechado automaticamente, mesmo com exceção!

O bloco with é a forma idiomática Python de garantir cleanup automático de recursos.

Criando context managers com classe

Qualquer classe que implemente __enter__ e __exit__ pode ser usada como context manager:

import time

class MedidorTempo:
    def __init__(self, nome):
        self.nome = nome
        self.inicio = None
        self.duracao = None

    def __enter__(self):
        self.inicio = time.perf_counter()
        return self  # O valor retornado fica disponível no 'as'

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.duracao = time.perf_counter() - self.inicio
        print(f"{self.nome}: {self.duracao:.4f}s")
        # exc_type, exc_val, exc_tb são None se não houve exceção
        # Retornar True suprime a exceção; False (ou None) a propaga
        return False

with MedidorTempo("Processamento") as m:
    time.sleep(0.5)
    dados = [x**2 for x in range(100_000)]

print(f"Duração armazenada: {m.duracao:.4f}s")
# Processamento: 0.5123s

O método __exit__ recebe três argumentos sobre possíveis exceções: o tipo, o valor e o traceback. Se retornar True, a exceção é suprimida; qualquer outro valor faz a exceção propagar normalmente.

Criando com @contextmanager (mais simples)

O decorator contextlib.contextmanager transforma uma função geradora em context manager, eliminando a necessidade de criar uma classe:

from contextlib import contextmanager
import tempfile
import os
import shutil

@contextmanager
def diretorio_temporario():
    """Context manager para criar e limpar diretório temporário."""
    tmpdir = tempfile.mkdtemp()
    try:
        yield tmpdir  # O código do bloco 'with' executa aqui
    finally:
        shutil.rmtree(tmpdir, ignore_errors=True)

@contextmanager
def transacao_banco(conexao):
    """Context manager para transações de banco com commit/rollback."""
    try:
        yield conexao
        conexao.commit()
        print("Transação confirmada")
    except Exception as e:
        conexao.rollback()
        print(f"Transação desfeita: {e}")
        raise

with diretorio_temporario() as tmpdir:
    # Trabalha com arquivos no diretório temporário
    arquivo = os.path.join(tmpdir, "dados.txt")
    with open(arquivo, 'w') as f:
        f.write("dados temporários")
    print(f"Arquivo em: {arquivo}")
# Diretório limpo automaticamente ao sair do bloco

Context managers assíncronos

Para código assíncrono com async/await, use context managers assíncronos com __aenter__ e __aexit__, ou @asynccontextmanager:

import asyncio
from contextlib import asynccontextmanager

class ConexaoAssincrona:
    def __init__(self, host):
        self.host = host
        self.conexao = None

    async def __aenter__(self):
        print(f"Conectando assincronamente a {self.host}...")
        await asyncio.sleep(0.1)  # Simula latência de conexão
        self.conexao = f"conn_{self.host}"
        return self.conexao

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        print("Fechando conexão assíncrona...")
        await asyncio.sleep(0.05)  # Simula cleanup
        self.conexao = None
        return False

@asynccontextmanager
async def sessao_http(url):
    """Context manager assíncrono para sessões HTTP."""
    print(f"Abrindo sessão para {url}")
    sessao = {"url": url, "ativa": True}
    try:
        yield sessao
    finally:
        sessao["ativa"] = False
        print(f"Sessão para {url} fechada")

async def main():
    async with ConexaoAssincrona("postgres://localhost") as conn:
        print(f"Usando: {conn}")

    async with sessao_http("https://api.exemplo.com") as sessao:
        print(f"Sessão ativa: {sessao['ativa']}")

asyncio.run(main())

contextlib: utilitários poderosos

O módulo contextlib oferece vários utilities prontos:

from contextlib import (
    suppress,
    redirect_stdout,
    redirect_stderr,
    ExitStack,
    closing,
    nullcontext,
)
import io

# suppress: ignora exceções específicas
with suppress(FileNotFoundError):
    os.remove("arquivo_que_pode_nao_existir.tmp")
# Nenhum erro — FileNotFoundError foi suprimido

# redirect_stdout: captura saída padrão
saida = io.StringIO()
with redirect_stdout(saida):
    print("Isso vai para a StringIO, não para o terminal")
    print("Linha 2")
conteudo = saida.getvalue()
print(f"Capturado: {conteudo!r}")

# closing: para objetos com .close() mas sem suporte a 'with'
class RecursoLegado:
    def buscar(self): return "dados"
    def close(self): print("Recurso legado fechado")

with closing(RecursoLegado()) as r:
    dados = r.buscar()
# Chamou r.close() automaticamente

# nullcontext: context manager que não faz nada (útil em condicionais)
def processar(dados, debug=False):
    ctx = MedidorTempo("debug") if debug else nullcontext()
    with ctx:
        return [x * 2 for x in dados]

ExitStack: context managers dinâmicos

ExitStack permite gerenciar múltiplos context managers de forma dinâmica, especialmente útil quando o número de recursos não é conhecido em tempo de compilação:

from contextlib import ExitStack

def processar_arquivos(caminhos):
    """Abre múltiplos arquivos e processa todos de uma vez."""
    with ExitStack() as stack:
        arquivos = [
            stack.enter_context(open(caminho, 'r', encoding='utf-8'))
            for caminho in caminhos
        ]
        # Todos os arquivos são fechados ao sair do bloco
        return [f.read() for f in arquivos]

# Outra aplicação: adicionar cleanup condicional
def conexao_com_fallback(usar_pool):
    stack = ExitStack()
    if usar_pool:
        pool = stack.enter_context(ConexaoPool())
        conn = pool.obter()
    else:
        conn = stack.enter_context(ConexaoSimples())
    return stack, conn

# Registro de callbacks de cleanup
with ExitStack() as stack:
    stack.callback(print, "Cleanup 1 executado")
    stack.callback(print, "Cleanup 2 executado")
    print("Dentro do bloco")
# Saída:
# Dentro do bloco
# Cleanup 2 executado  (LIFO)
# Cleanup 1 executado

Context managers em testes

Context managers são amplamente usados em testes:

import unittest
from unittest.mock import patch, MagicMock
from contextlib import contextmanager

# pytest.raises — verifica que uma exceção é levantada
import pytest

def dividir(a, b):
    if b == 0:
        raise ZeroDivisionError("Divisão por zero")
    return a / b

def test_divisao_por_zero():
    with pytest.raises(ZeroDivisionError, match="Divisão por zero"):
        dividir(10, 0)

# unittest.mock.patch — substitui objetos durante o teste
def test_chamada_api():
    with patch('requests.get') as mock_get:
        mock_get.return_value = MagicMock(
            status_code=200,
            json=lambda: {"status": "ok"}
        )
        # Código que usa requests.get será interceptado
        resultado = chamar_api("https://exemplo.com")
        mock_get.assert_called_once_with("https://exemplo.com")

@contextmanager
def banco_em_memoria():
    """Context manager para testes com banco de dados em memória."""
    import sqlite3
    conn = sqlite3.connect(':memory:')
    conn.execute("CREATE TABLE usuarios (id INTEGER, nome TEXT)")
    try:
        yield conn
    finally:
        conn.close()

def test_inserir_usuario():
    with banco_em_memoria() as db:
        db.execute("INSERT INTO usuarios VALUES (1, 'Ana')")
        cursor = db.execute("SELECT nome FROM usuarios WHERE id=1")
        assert cursor.fetchone()[0] == 'Ana'

Padrões reais de uso

Gerenciamento de locks

import threading
from contextlib import contextmanager

@contextmanager
def lock_timeout(lock, timeout=5.0):
    """Lock com timeout — evita deadlocks infinitos."""
    adquirido = lock.acquire(timeout=timeout)
    if not adquirido:
        raise TimeoutError(f"Não foi possível adquirir o lock em {timeout}s")
    try:
        yield
    finally:
        lock.release()

_lock = threading.Lock()

def operacao_critica():
    with lock_timeout(_lock, timeout=2.0):
        # Seção crítica protegida
        pass

Mudança temporária de estado

@contextmanager
def variavel_de_ambiente(nome, valor):
    """Altera uma variável de ambiente temporariamente."""
    valor_original = os.environ.get(nome)
    os.environ[nome] = valor
    try:
        yield
    finally:
        if valor_original is None:
            del os.environ[nome]
        else:
            os.environ[nome] = valor_original

@contextmanager
def diretorio_atual(caminho):
    """Muda o diretório de trabalho temporariamente."""
    anterior = os.getcwd()
    os.chdir(caminho)
    try:
        yield caminho
    finally:
        os.chdir(anterior)

with variavel_de_ambiente("DEBUG", "true"):
    print(os.environ["DEBUG"])  # true
# DEBUG voltou ao valor original

Context managers aninhados

# Múltiplos context managers na mesma linha (Python 3.1+)
with open('entrada.txt') as entrada, open('saida.txt', 'w') as saida:
    for linha in entrada:
        saida.write(linha.upper())

# Python 3.10+: parênteses para múltiplas linhas
with (
    open('log.txt', 'a') as log,
    ConexaoBanco("localhost") as db,
    MedidorTempo("importacao") as timer,
):
    dados = db.buscar_todos()
    log.write(f"Buscados {len(dados)} registros\n")

Erros comuns

O erro mais frequente é levantar exceções dentro de __enter__ sem cleanup adequado. Outro problema comum é retornar True em __exit__ acidentalmente (suprimindo exceções sem intenção). No @contextmanager, esquecer o try/finally ao redor do yield faz com que exceções no bloco with sejam ignoradas e o cleanup não execute:

# ERRADO — sem try/finally, exceções pulam o cleanup
@contextmanager
def arquivo_temporario():
    f = open('temp.txt', 'w')
    yield f
    f.close()  # Não executa se houver exceção!

# CORRETO — try/finally garante cleanup sempre
@contextmanager
def arquivo_temporario():
    f = open('temp.txt', 'w')
    try:
        yield f
    finally:
        f.close()  # Sempre executa

Quando usar context managers?

Use context managers sempre que houver setup e teardown: abrir/fechar recursos, iniciar/finalizar transações, adquirir/liberar locks, salvar/restaurar estado. Se você se pega escrevendo try/finally para cleanup de recursos, provavelmente deveria estar usando um context manager.

Termos Relacionados