Chapter 38, Intermediate
Context managers
The `with` statement ensures that something is undone at the end, no matter what happens. Files, connections, locks, and temporary directories use the same mechanism.
Code for this chapter: intermediario/cap38_context_managers.py
The protocol
Any object with __enter__ and __exit__ works with with. The __enter__ prepares and returns the resource. The __exit__ cleans up, and receives the exception data, if any:
import time
class Cronometro:
def __enter__(self):
self.inicio = time.perf_counter()
return self
def __exit__(self, tipo_exc, valor_exc, traceback):
self.duracao = time.perf_counter() - self.inicio
return False
with Cronometro() as cron:
sum(range(100_000))
print(cron.duracao >= 0)
True
The value that the __exit__ returns decides the fate of the exception: False lets it continue, True the suppresses. The __exit__ runs even when the block fails:
class Ignorar:
def __enter__(self):
return self
def __exit__(self, tipo_exc, valor_exc, traceback):
print("saindo, erro:", tipo_exc.__name__ if tipo_exc else None)
return tipo_exc is ZeroDivisionError
with Ignorar():
1 / 0
print("continuou")
saindo, erro: ZeroDivisionError
continuou
The short form: contextmanager
Writing a class just for that is overkill. The decorator contextlib.contextmanager transforms a generator with a single yield into a context manager. Everything before the yield is the __enter__. Everything after is the __exit__. Put the cleanup in a finally:
from contextlib import contextmanager
@contextmanager
def secao(titulo):
print(f"[início] {titulo}")
try:
yield
finally:
print(f"[fim] {titulo}")
with secao("importação"):
print("trabalhando")
[início] importação
trabalhando
[fim] importação
Ready-made tools
The standard library already includes the most common cases. The suppress explicitly ignores a specific exception, and the TemporaryDirectory creates a folder that disappears at the end:
from contextlib import suppress
from pathlib import Path
import tempfile
with suppress(FileNotFoundError):
Path("nao_existe.txt").unlink()
print("sem erro")
with tempfile.TemporaryDirectory() as pasta:
arquivo = Path(pasta) / "tmp.txt"
arquivo.write_text("oi", encoding="utf-8")
print(arquivo.exists())
print(Path(pasta).exists())
sem erro
True
False
Exercise 1
Change directory and return
Write mudar_pasta(destino), a context manager that changes the working directory and always restores it at the end, even if the block fails.