Python Gerenciadores de Contexto (with)

Desde a aula Abrindo Arquivos, você abre arquivos com with open(…) as arquivo: e confia que o Python vai fechá-los no fim. Mas quem fecha? E como o with sabe o que fazer com um arquivo? Nesta aula você vai ver o with por dentro: os dois métodos especiais que ele chama, __enter__ e __exit__, e o atalho do módulo contextlib para escrever os seus com um gerador.

Você vai aprender
  • O que é um gerenciador de contexto e o problema que ele resolve.
  • O que o with faz por dentro, e por que ele é um try/finally disfarçado.
  • A criar o seu com __enter__ e __exit__, e o que o as recebe.
  • O que o __exit__ recebe quando dá erro, e como (e quando não) engolir a exceção.
  • A escrever um gerenciador com @contextmanager, yield e try/finally.
  • suppress e vários gerenciadores no mesmo with.
  • Os erros reais: does not support the context manager protocol, I/O operation on closed file, generator didn't yield e a limpeza que não acontece.

A ideia: a luz do quarto

Ao entrar no quarto, você acende a luz; ao sair, apaga. Tem gente que sempre esquece de apagar, principalmente quando sai com pressa (o telefone tocou, a panela ferveu). Uma solução é o sensor de presença: ele acende quando você entra e apaga quando você sai, não importa como você saiu.

Muita coisa em programação tem esse formato "preparar, usar, arrumar": abrir e fechar um arquivo, conectar e desconectar de um banco de dados, travar e destravar um recurso. O gerenciador de contexto é o sensor de presença: o with marca o "quarto" (o bloco indentado), e o gerenciador cuida de preparar na entrada e de arrumar na saída, até quando a saída é por um erro.

MomentoNa luz do quartoNo arquivoMétodo chamado
Entrar no bloco withAcende a luzAbre o arquivo__enter__
Dentro do blocoVocê lê o livroVocê lê e escreve(o seu código)
Sair do bloco (normal ou por erro)Apaga a luzFecha o arquivo__exit__

1. O with que você já conhece

O atributo closed ("fechado") de um arquivo mostra se ele já foi fechado. Olhe o valor dentro e fora do with:

with open("compras.txt", "w", encoding="utf-8") as arquivo:
    arquivo.write("pão\nleite\n")
    print("Dentro do with, fechado?", arquivo.closed)

print("Depois do with, fechado?", arquivo.closed)
$ python3 fechado.py
Dentro do with, fechado? False
Depois do with, fechado? True

Ninguém escreveu arquivo.close(), e mesmo assim, ao sair do bloco, o arquivo estava fechado. Repare também que a variável arquivo continua existindo depois do with: o que acabou foi o "contexto", não a variável.

O jeito antigo: try e finally

Sem o with, para garantir o fechamento mesmo quando algo dá errado, é preciso um try/finally (aula else e finally). Este programa erra de propósito na linha 4: tenta gravar um número, e write só aceita texto.

arquivo = open("compras.txt", "w", encoding="utf-8")
try:
    arquivo.write("pão\n")
    arquivo.write(10)
finally:
    arquivo.close()
    print("Fechado?", arquivo.closed)
$ python3 sem_with.py
Fechado? True
Traceback (most recent call last):
  File "/home/ana/sem_with.py", line 4, in <module>
    arquivo.write(10)
    ~~~~~~~~~~~~~^^^^
TypeError: write() argument must be str, not int

O finally rodou antes de o erro aparecer: primeiro o Python arrumou a casa (Fechado? True), depois mostrou o traceback e parou. É exatamente isso que o with faz por você, em uma linha em vez de cinco.


2. O seu gerenciador: __enter__ e __exit__

Qualquer objeto pode ir num with, desde que tenha dois métodos especiais (aula Métodos Especiais). A padaria da Ana abre e fecha todo dia; um gerenciador de contexto garante que ela nunca fique aberta:

class Expediente:
    def __init__(self, loja):
        self.loja = loja

    def __enter__(self):
        print(f"Abrindo a {self.loja}.")
        return self

    def __exit__(self, tipo, valor, rastro):
        print(f"Fechando a {self.loja}.")
        return False

    def vender(self, produto):
        print(f"  Vendido: {produto}")


with Expediente("padaria") as dia:
    dia.vender("pão")
    dia.vender("café")

print("Fim do dia.")
$ python3 loja.py
Abrindo a padaria.
  Vendido: pão
  Vendido: café
Fechando a padaria.
Fim do dia.
with Expediente("padaria")
Cria o objeto, como sempre, e entrega para o with.
def __enter__(self):
"Entrar". O with chama este método antes da primeira linha do bloco. É o "preparar".
return self
O que o __enter__ devolve vai para a variável do as. Aqui, o próprio objeto: por isso dia.vender(…) funciona. (O open() também devolve o próprio arquivo.)
as dia
O nome que recebe o retorno do __enter__. É opcional: with Expediente("padaria"): também vale, se você não precisar dele.
def __exit__(self, tipo, valor, rastro):
"Sair". O with chama este método quando o bloco acaba, de qualquer jeito. É o "arrumar". Os três parâmetros contam se houve erro (seção 3).
return False
"Não engoli nenhum erro": se uma exceção aconteceu no bloco, ela continua subindo. Na seção 3 você vê o True.

O with por dentro

O with do loja.py faz, escondido, mais ou menos isto:

class Expediente:
    def __init__(self, loja):
        self.loja = loja

    def __enter__(self):
        print(f"Abrindo a {self.loja}.")
        return self

    def __exit__(self, tipo, valor, rastro):
        print(f"Fechando a {self.loja}.")
        return False


gerente = Expediente("padaria")
dia = gerente.__enter__()
try:
    print("  vendendo...")
finally:
    gerente.__exit__(None, None, None)
$ python3 por_dentro.py
Abrindo a padaria.
  vendendo...
Fechando a padaria.
  1. Avalia a expressão depois do with e guarda o gerenciador.
  2. Chama __enter__() e põe o resultado na variável do as.
  3. Roda o bloco dentro de um try.
  4. No fim, chama __exit__: com None, None, None se tudo correu bem; com os dados da exceção se deu erro. (O with de verdade ainda decide, pelo retorno do __exit__, se a exceção continua.)

Missão: a classe Luz só tem o __enter__. Rode e leia o erro. Depois escreva o __exit__(self, tipo, valor, rastro), que mostra Luz apagada. e devolve False.

class Luz:
    def __enter__(self):
        print("Luz acesa.")
        return self


with Luz():
    print("Lendo o livro...")
class Luz:
    def __enter__(self):
        print("Luz acesa.")
        return self

    def __exit__(self, tipo, valor, rastro):
        print("Luz apagada.")
        return False


with Luz():
    print("Lendo o livro...")

3. Quando dá erro dentro do bloco

A promessa do with é: o __exit__ roda sempre. Vamos testar com uma divisão por zero no meio do expediente. Este __exit__ também mostra o que recebeu:

class Expediente:
    def __init__(self, loja):
        self.loja = loja

    def __enter__(self):
        print(f"Abrindo a {self.loja}.")
        return self

    def __exit__(self, tipo, valor, rastro):
        print(f"Fechando a {self.loja}.")
        print(f"  (tipo={tipo}, valor={valor!r})")
        return False

    def vender(self, produto):
        print(f"  Vendido: {produto}")


with Expediente("padaria") as dia:
    dia.vender("pão")
    troco = 10 / 0
    dia.vender("café")

print("Fim do dia.")
$ python3 loja_erro.py
Abrindo a padaria.
  Vendido: pão
Fechando a padaria.
  (tipo=<class 'ZeroDivisionError'>, valor=ZeroDivisionError('division by zero'))
Traceback (most recent call last):
  File "/home/ana/loja_erro.py", line 20, in <module>
    troco = 10 / 0
            ~~~^~~
ZeroDivisionError: division by zero
  • O dia.vender("café") não rodou: o erro da linha 20 interrompeu o bloco.
  • Mas a padaria fechou: o __exit__ rodou antes de o traceback aparecer.
  • O __exit__ recebeu o tipo da exceção (ZeroDivisionError), o valor (a exceção em si, com a mensagem) e o rastro (o traceback, um objeto que aqui não mostramos). O !r na f-string mostra o valor como o Python o escreveria no código.
  • Como o __exit__ devolveu False, a exceção seguiu o caminho normal: traceback, programa parado, e o Fim do dia. nunca apareceu.

Engolindo a exceção: return True

Se o __exit__ devolver True, o with entende "eu cuidei do erro" e a exceção some. O programa segue depois do bloco:

class IgnorarDivisaoPorZero:
    def __enter__(self):
        return self

    def __exit__(self, tipo, valor, rastro):
        if tipo is ZeroDivisionError:
            print("Divisão por zero ignorada.")
            return True
        return False


with IgnorarDivisaoPorZero():
    print("Antes da conta")
    media = 10 / 0
    print("Esta linha não roda")

print("O programa continuou.")
$ python3 engolir.py
Antes da conta
Divisão por zero ignorada.
O programa continuou.
Engula só o que você esperava

Repare no if tipo is ZeroDivisionError: o gerenciador só engole aquele erro. Um __exit__ que sempre devolve True esconde todos os erros do bloco, inclusive os que você não imaginou (um nome digitado errado, um TypeError), e o programa segue com dados errados sem avisar ninguém. Na dúvida, devolva False. E lembre: a linha que deu erro e as seguintes, dentro do bloco, não rodam (Esta linha não roda).


4. O atalho: @contextmanager

Escrever uma classe com dois métodos para "abrir e fechar a padaria" é muita cerimônia. O módulo contextlib tem o decorador @contextmanager (aula Decoradores) que transforma um gerador (aula Geradores) em gerenciador de contexto:

from contextlib import contextmanager


@contextmanager
def expediente(loja):
    print(f"Abrindo a {loja}.")
    try:
        yield loja.upper()
    finally:
        print(f"Fechando a {loja}.")


with expediente("padaria") as placa:
    print("  Placa na porta:", placa)

print("---")

with expediente("farmácia"):
    print("  Vendendo...")
    raise ValueError("faltou troco")
$ python3 gerador.py
Abrindo a padaria.
  Placa na porta: PADARIA
Fechando a padaria.
---
Abrindo a farmácia.
  Vendendo...
Fechando a farmácia.
Traceback (most recent call last):
  File "/home/ana/gerador.py", line 20, in <module>
    raise ValueError("faltou troco")
ValueError: faltou troco
@contextmanager
Escreve o __enter__ e o __exit__ para você, a partir do gerador.
o que vem antes do yield
É o __enter__: roda na entrada do with.
yield loja.upper()
O ponto em que o bloco do with roda. O valor entregue pelo yield vai para o as (aqui, "PADARIA"). Um yield sozinho entrega None.
try: … finally:
Se o bloco do with der erro, a exceção "aparece" dentro do gerador, na linha do yield. Sem o try/finally, a limpeza seria pulada (veja em Quando dá errado).
o que vem depois do yield (no finally)
É o __exit__: roda na saída do with, de qualquer jeito.

Na farmácia, o raise (aula raise) lançou um erro dentro do bloco: o finally fechou a farmácia, e o erro seguiu até o traceback.


5. Prontos para usar, e vários juntos

suppress: ignorar um erro esperado

O contextlib.suppress ("suprimir") é um gerenciador pronto que engole os tipos de exceção que você disser, como o IgnorarDivisaoPorZero da seção 3. Útil para "apague o rascunho, se ele existir":

from contextlib import suppress
import os

with suppress(FileNotFoundError):
    os.remove("rascunho.txt")
    print("Apaguei o rascunho.")

print("Seguindo em frente.")
$ python3 suppress.py
Seguindo em frente.                « não havia rascunho: o erro foi engolido
$ echo "rascunho" > rascunho.txt
$ python3 suppress.py
Apaguei o rascunho.
Seguindo em frente.

Na primeira vez, o os.remove lançou FileNotFoundError, o suppress engoliu, e a linha Apaguei o rascunho. foi pulada. É o mesmo que um try com except FileNotFoundError: pass, em menos linhas.

Dois arquivos no mesmo with

Para copiar de um arquivo para outro, os dois precisam estar abertos ao mesmo tempo. Um with aceita vários gerenciadores, separados por vírgula; entre parênteses, cada um fica numa linha:

with open("entrada.txt", "w", encoding="utf-8") as f:
    f.write("pão\nleite\ncafé\n")

with (
    open("entrada.txt", encoding="utf-8") as origem,
    open("saida.txt", "w", encoding="utf-8") as destino,
):
    for linha in origem:
        destino.write(linha.upper())

with open("saida.txt", encoding="utf-8") as f:
    print(f.read(), end="")
$ python3 dois.py
PÃO
LEITE
CAFÉ

Os dois são abertos na ordem em que aparecem e fechados na ordem contrária, ao fim do bloco. A forma com parênteses existe desde o Python 3.10; num Python mais antigo, escreva tudo numa linha só: with open(…) as origem, open(…) as destino:.


Formas diferentes de garantir a limpeza

FormaQuando usar
try / finally na mãoUma limpeza que aparece uma vez só no programa
with com um gerenciador prontoSempre que existe um: open, suppress, conexões de banco (aula sqlite3)
@contextmanager com yieldO seu "preparar e arrumar" de todo dia: curto, e lê de cima para baixo
Classe com __enter__ e __exit__Quando o gerenciador é um objeto com outros métodos (como o dia.vender), ou precisa decidir com cuidado que exceções engolir

Quando dá errado

Um objeto que não é gerenciador de contexto
class Expediente:
    def __init__(self, loja):
        self.loja = loja


with Expediente("padaria") as dia:
    print("vendendo")
$ python3 erro_protocolo.py
Traceback (most recent call last):
  File "/home/ana/erro_protocolo.py", line 6, in <module>
    with Expediente("padaria") as dia:
         ~~~~~~~~~~^^^^^^^^^^^
TypeError: 'Expediente' object does not support the context manager protocol (missed __exit__ method)

"O objeto Expediente não segue o protocolo de gerenciador de contexto (faltou o método __exit__)". Protocolo aqui quer dizer "o combinado": ter __enter__ e __exit__. Esta classe não tem nenhum dos dois, mas a mensagem cita só o __exit__. Na classe que só tem o __exit__, o fim da mensagem muda para (missed __enter__ method). Como consertar: escreva os dois métodos, ou use @contextmanager.

Usar o arquivo depois do with
with open("compras.txt", "w", encoding="utf-8") as arquivo:
    arquivo.write("pão\n")

arquivo.write("leite\n")
$ python3 erro_fechado.py
Traceback (most recent call last):
  File "/home/ana/erro_fechado.py", line 4, in <module>
    arquivo.write("leite\n")
    ~~~~~~~~~~~~~^^^^^^^^^^^
ValueError: I/O operation on closed file.

"Operação de entrada/saída num arquivo fechado". A linha 4 está fora do bloco (sem indentação), e lá o arquivo já foi fechado pelo __exit__. Como consertar: indente a linha para dentro do with, ou abra o arquivo de novo (com "a", de append, para acrescentar no fim).

O __enter__ esqueceu o return self

O Expediente do loja.py, com o __enter__ sem a linha return self:

$ python3 erro_enter_none.py
Abrindo a padaria.
Fechando a padaria.
Traceback (most recent call last):
  File "/home/ana/erro_enter_none.py", line 17, in <module>
    dia.vender("pão")
    ^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'vender'

"O objeto NoneType não tem o atributo vender". Sem return, o __enter__ devolveu None, e foi o None que o as dia recebeu. O dia.vender falhou na linha 17, e mesmo assim a padaria fechou. Como consertar: return self no fim do __enter__.

@contextmanager sem try/finally: a limpeza não acontece
from contextlib import contextmanager


@contextmanager
def expediente(loja):
    print(f"Abrindo a {loja}.")
    yield
    print(f"Fechando a {loja}.")


with expediente("farmácia"):
    print("  Vendendo...")
    raise ValueError("faltou troco")
$ python3 gerador_sem_finally.py
Abrindo a farmácia.
  Vendendo...
Traceback (most recent call last):
  File "/home/ana/gerador_sem_finally.py", line 13, in <module>
    raise ValueError("faltou troco")
ValueError: faltou troco

Faltou o Fechando a farmácia.! O erro do bloco foi lançado dentro do gerador, na linha do yield, e interrompeu a função ali mesmo: o print de depois nunca rodou. Sem erro, funcionaria; é justamente no dia do erro que a farmácia fica aberta. Como consertar: ponha o yield dentro de um try e a limpeza no finally, como no gerador.py.

O gerador não chegou ao yield
from contextlib import contextmanager


@contextmanager
def expediente(loja):
    print(f"Abrindo a {loja}.")
    if loja == "padaria":
        yield
    print(f"Fechando a {loja}.")


with expediente("farmácia"):
    print("  Vendendo...")
$ python3 erro_sem_yield.py
Abrindo a farmácia.
Fechando a farmácia.
Traceback (most recent call last):
  File "/home/ana/erro_sem_yield.py", line 12, in <module>
    with expediente("farmácia"):
         ~~~~~~~~~~^^^^^^^^^^^^
  File "/usr/lib/python3.14/contextlib.py", line 143, in __enter__
    raise RuntimeError("generator didn't yield") from None
RuntimeError: generator didn't yield

"O gerador não fez yield". Para a farmácia, o if pulou o yield, e a função terminou sem entregar o ponto em que o bloco deveria rodar. O traceback tem um segundo trecho, dentro do contextlib.py: é o __enter__ que o @contextmanager escreveu para você. O Vendendo... não apareceu: o bloco nem começou. Com dois yield seguidos, o erro é o contrário: RuntimeError: generator didn't stop ("o gerador não parou"). Como consertar: o gerador de um @contextmanager tem de passar por exatamente um yield, sempre.


Referência rápida

EscritaO que faz
with gerenciador as nome:Chama __enter__, guarda o retorno em nome, roda o bloco e chama __exit__ no fim, sempre
def __enter__(self): … return selfO "preparar"; o retorno vai para o as
def __exit__(self, tipo, valor, rastro):O "arrumar"; recebe None, None, None ou os dados da exceção
return False / return True no __exit__Deixa a exceção seguir / engole a exceção
@contextmanager + try: yield + finally:Gerenciador a partir de um gerador (de contextlib)
with suppress(Erro):Engole as exceções do tipo Erro (de contextlib)
with (a as x, b as y):Vários gerenciadores; fecham na ordem contrária (3.10+)
SintomaCausa provável
does not support the context manager protocol (missed __exit__ method)Faltam o __enter__ e/ou o __exit__
ValueError: I/O operation on closed file.Usou o arquivo fora do bloco with
AttributeError: 'NoneType' object has no attribute … no asO __enter__ não devolve nada
A limpeza não roda quando dá erro@contextmanager sem try/finally em volta do yield
RuntimeError: generator didn't yield / didn't stopO gerador passou por zero / por mais de um yield
Um erro "sumiu" sem avisoO __exit__ devolve True sempre

Exercícios

1. Os dois métodos

Escreva os nomes dos dois métodos especiais que o with chama, na ordem:

Na entrada: · Na saída:

2. O que vai para o as

Complete o __enter__ para que with Expediente("padaria") as dia: receba o próprio objeto:

def __enter__(self):
    

3. Ignorando um erro

Complete para apagar velho.txt sem dar erro se ele não existir:

with :
    os.remove("velho.txt")

4. Uma exceção acontece na segunda linha de um bloco with, e o __exit__ devolve False. O que acontece?

5. Num @contextmanager, onde fica o código que deve rodar mesmo se der erro no bloco do with?

6. Por que um __exit__ que sempre devolve True é perigoso?

Missão final: o caixa sempre fecha

Missão: num dia sem clientes, a conta da média divide por zero, e o caixa fica aberto: o Caixa fechado. não aparece. Proteja a limpeza: ponha o yield dentro de um try: e o print("Caixa fechado.") num finally:. O erro vai continuar aparecendo (ele é de verdade), mas agora depois de o caixa fechar.

from contextlib import contextmanager


@contextmanager
def caixa():
    print("Caixa aberto.")
    yield
    print("Caixa fechado.")


with caixa():
    clientes = 0
    print("Média por cliente:", 120 / clientes)
from contextlib import contextmanager


@contextmanager
def caixa():
    print("Caixa aberto.")
    try:
        yield
    finally:
        print("Caixa fechado.")


with caixa():
    clientes = 0
    print("Média por cliente:", 120 / clientes)

Com o finally, a saída fica Caixa aberto., Caixa fechado. e, por último, o traceback do ZeroDivisionError. Para tratar o erro de verdade (mostrar "sem clientes hoje" em vez do traceback), use um if clientes == 0 ou um try/except dentro do bloco.


Resumo

  • Um gerenciador de contexto cuida do "preparar e arrumar" em volta de um bloco: o with garante a arrumação, até quando o bloco termina por erro.
  • O with chama __enter__ na entrada (o retorno vai para o as) e __exit__ na saída. Por dentro, é um try/finally.
  • O __exit__ recebe tipo, valor e rastro da exceção (ou três None). Devolvendo True, engole a exceção; faça isso só com o erro que você esperava.
  • @contextmanager faz um gerenciador a partir de um gerador: antes do yield é a entrada, o finally é a saída. Sem o try/finally, a limpeza é pulada quando dá erro.
  • suppress(Erro) engole um erro esperado; um with aceita vários gerenciadores, que fecham na ordem contrária.