Fiecare începător în Python învață context managers la fel. Ziua unu: deschide un fișier cu open(...). Ziua doi: cineva spune „folosește with open(...) as f: în loc, închide fișierul automat”. Ăsta e tot modelul mental pe care majoritatea oamenilor îl plimbă cu ei ani la rând.
E un punct de plecare bun. E și cam 5% din ce face with. Același protocol gestionează tranzacții de baze de date, achiziție de lock-uri, directoare temporare care se curăță singure, redirecționare de stdout, blocuri de timing, suprimare de excepții și zeci de pattern-uri în care trebuie să faci X la intrare și Y la ieșire, garantat, chiar dacă corpul blocului ridică o excepție.
Astăzi ne uităm la protocolul de dedesubt, la trei moduri de a-ți scrie propriul context manager și la cazurile în care with e dramatic mai plăcut decât try/finally.
Protocolul
Un context manager e orice obiect cu două metode:
__enter__(self): chemat când execuția intră în bloculwith. Ce returnează e legat la variabila de dupăas.__exit__(self, exc_type, exc_value, traceback): chemat când execuția părăsește blocul, fie normal, fie via o excepție.
Ăsta e tot protocolul. PEP 343 l-a oficializat; tot restul sunt biblioteci construite peste.
class Sandbox:
def __enter__(self) -> "Sandbox":
print("entering")
return self
def __exit__(self, exc_type, exc_value, traceback) -> None:
print(f"exiting (exception: {exc_type})")
with Sandbox() as box:
print("inside")
raise ValueError("oops")
# entering
# inside
# exiting (exception: <class 'ValueError'>)
# Traceback (most recent call last):
# ...
# ValueError: oops
Chiar dacă corpul a ridicat o excepție, __exit__ a rulat. Asta e garanția pe care ți-o dă with. E aceeași garanție pe care ți-o dă try/finally, dar cu mult mai puțin cod pe pagină și un semnal mai clar al intenției.
Valoarea de retur din __exit__ (nu te-ncurca aici)
__exit__ poate returna o valoare. Dacă returnează o valoare truthy, excepția e suprimată: execuția continuă după blocul with ca și cum nu s-ar fi întâmplat nimic. Dacă returnează None sau False, excepția se propagă normal.
class SwallowEverything:
def __enter__(self) -> "SwallowEverything":
return self
def __exit__(self, exc_type, exc_value, traceback) -> bool:
return True # suprimă toate excepțiile
with SwallowEverything():
raise ValueError("ignored silently")
print("we get here")
În 99% din cazuri, nu vrei asta. Înghițirea tăcută a excepțiilor ascunde bug-uri. Utilizările legitime sunt înguste: contextlib.suppress, handler-e de rollback de tranzacții care reraise după curățare, decoratori de retry construiți ca context managers. Dacă te trezești scriind return True dintr-un __exit__, oprește-te și întreabă-te dacă chiar asta vrei. De obicei, întoarcerea lui None (default-ul) e corectă.
Modul 1: o clasă cu __enter__ / __exit__
Forma cea mai explicită. Cea mai bună atunci când managerul are stare care trăiește între enter/exit, sau când utilizatorul are nevoie să cheme metode pe el în interiorul blocului.
import time
from typing import Optional
class Timer:
def __init__(self, label: str) -> None:
self.label = label
self.elapsed_ms: float = 0.0
self._start: Optional[float] = None
def __enter__(self) -> "Timer":
self._start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_value, traceback) -> None:
assert self._start is not None
self.elapsed_ms = (time.perf_counter() - self._start) * 1000
print(f"{self.label}: {self.elapsed_ms:.2f} ms")
with Timer("import csv") as t:
import csv
# ... fă treabă ...
print(f"that took {t.elapsed_ms:.2f} ms total")
Instanța Timer supraviețuiește dincolo de blocul with: t.elapsed_ms e încă citibil. Ăsta e cazul pentru forma de clasă: când ai nevoie ca managerul să fie un obiect real cu care interacționezi după.
Modul 2: @contextlib.contextmanager pe un generator
Pentru cazul comun în care __enter__ e scurt și __exit__ e scurt, forma de clasă e exagerată. contextlib îți dă un decorator care transformă o funcție generator într-un context manager. Tot ce e înainte de yield e __enter__. Tot ce e după yield e __exit__. Valoarea livrată cu yield e ce leagă as.
from contextlib import contextmanager
from typing import Iterator
import os
@contextmanager
def in_directory(path: str) -> Iterator[None]:
previous = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(previous)
with in_directory("/tmp"):
# ... lucrează în /tmp ...
pass
# înapoi în directorul original, chiar dacă blocul a ridicat o excepție
try/finally e obligatoriu aici. Dacă corpul ridică o excepție, controlul se întoarce la generator, la locul yield, ca o excepție: fără finally, os.chdir(previous) ar fi sărit, iar tu ai scurge schimbarea de director de lucru. Ăsta e cel mai comun bug în context managers scriși de mână; pytest îl prinde prima dată când ceva aruncă.
O tranzacție de bază de date e exemplul de manual:
from contextlib import contextmanager
from typing import Iterator
@contextmanager
def transaction(conn) -> Iterator[None]:
cursor = conn.cursor()
try:
cursor.execute("BEGIN")
yield
cursor.execute("COMMIT")
except Exception:
cursor.execute("ROLLBACK")
raise
finally:
cursor.close()
with transaction(conn):
cursor = conn.cursor()
cursor.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
cursor.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
# COMMIT la succes, ROLLBACK la eșec. Cursorul mereu închis.
Ăsta e pattern-ul pe care îl expun majoritatea ORM-urilor și driver-elor de DB sub nume de tipul session.begin() sau engine.begin(). Oasele sunt mereu aceleași.
Modul 3: contextlib.ExitStack pentru contexte dinamice
Uneori nu știi la write-time câte contexte vei avea nevoie. Deschizi un fișier per rând dintr-o configurare. Sau N conexiuni la baza de date pe baza unor argumente de runtime. Sau un număr variabil de lock-uri. Nu poți scrie with a, b, c: pentru că nu știi câți a, b, c sunt.
ExitStack e răspunsul:
from contextlib import ExitStack
from typing import IO
def merge_files(paths: list[str], output: str) -> None:
with ExitStack() as stack:
inputs: list[IO[str]] = [
stack.enter_context(open(p, encoding="utf-8"))
for p in paths
]
out = stack.enter_context(open(output, "w", encoding="utf-8"))
for f in inputs:
for line in f:
out.write(line)
# Toate fișierele închise aici, în ordine inversă, chiar și la excepție.
stack.enter_context(cm) e echivalent cu a intra în cm și a-i înregistra __exit__ să fie chemat când stack-ul se desface. Poți înregistra și callback-uri arbitrare:
import shutil
import tempfile
from contextlib import ExitStack
def process_with_workspace(input_path: str) -> None:
with ExitStack() as stack:
workspace = tempfile.mkdtemp()
stack.callback(shutil.rmtree, workspace, ignore_errors=True)
# ... lucrează în `workspace` ...
# workspace e curățat chiar dacă blocul a ridicat o excepție
ExitStack e și unealta corectă atunci când vrei unele curățări condiționate de ce s-a întâmplat mai devreme în bloc: poți folosi pop_all() pentru a detașa curățările înregistrate și a le rula mai târziu (sau niciodată).
Built-ins utile din contextlib
from contextlib import suppress, redirect_stdout, nullcontext
import io
# 1. suppress: ignoră un tip specific de excepție
with suppress(FileNotFoundError):
os.remove("maybe-doesnt-exist.txt")
# 2. redirect_stdout: capturează output-ul de la print()
buf = io.StringIO()
with redirect_stdout(buf):
print("captured")
print(buf.getvalue()) # "captured\n"
# 3. nullcontext: un context manager fără efect - util când vrei
# condițional să intri într-un context real sau în niciunul
def maybe_lock(use_lock: bool, lock):
cm = lock if use_lock else nullcontext()
with cm:
do_critical_work()
suppress e cazul de utilizare legitim pentru un __exit__ care returnează True: pentru un singur tip de excepție specific, numit. Nu îl folosi pentru „ignoră tot”; folosește-l pentru „această excepție exactă e așteptată și înseamnă că operația e un no-op”.
Exemple din lumea reală pe care le-am folosit
Un wrapper „log și reraise” pentru granița unui serviciu:
from contextlib import contextmanager
from typing import Iterator
import logging
import time
@contextmanager
def request_context(name: str, logger: logging.Logger) -> Iterator[None]:
start = time.perf_counter()
logger.info("start", extra={"op": name})
try:
yield
except Exception:
elapsed = (time.perf_counter() - start) * 1000
logger.exception("failed", extra={"op": name, "ms": elapsed})
raise
else:
elapsed = (time.perf_counter() - start) * 1000
logger.info("ok", extra={"op": name, "ms": elapsed})
with request_context("export-orders", log):
run_export()
Un override temporar de feature flag pentru testare:
from contextlib import contextmanager
from typing import Iterator
@contextmanager
def feature(flag: str, value: bool) -> Iterator[None]:
previous = flags.get(flag)
flags[flag] = value
try:
yield
finally:
if previous is None:
del flags[flag]
else:
flags[flag] = previous
def test_new_export() -> None:
with feature("new_export_pipeline", True):
result = run_export()
assert result.path.endswith(".parquet")
Un lock cu domeniu, care înregistrează cine îl deține:
from contextlib import contextmanager
from threading import RLock
from typing import Iterator
_lock = RLock()
_holder: str | None = None
@contextmanager
def held_by(name: str) -> Iterator[None]:
global _holder
_lock.acquire()
_holder = name
try:
yield
finally:
_holder = None
_lock.release()
(Notă laterală: acel global e o capcană în cod concurent real; e ok pentru diagnosticare, nu pentru corectitudine.)
Async context managers (preview)
asyncio are propria versiune. Protocolul e __aenter__ / __aexit__, ambele async, iar sintaxa e async with:
async with session.get(url) as response:
body = await response.read()
Aceeași idee, async până jos. Vom acoperi-o cum trebuie în lecția 40, când ajungem la I/O async: pattern-urile se transferă aproape verbatim.
Când with bate try/finally
Ambele sunt corecte. Motivele de a prefera with:
- Intenție.
with transaction(conn):spune exact ce se întâmplă.try: ... finally:spune „ceva are nevoie de curățare, derulează în jos ca să afli ce”. - Reutilizare. Un context manager pe care-l scrii o dată e reutilizabil în douăzeci de locuri. Un bloc
try/finallye copy-paste-uit în douăzeci de locuri. - Compoziție.
with a, b, c:intră în trei manageri în ordine, iese din ei în ordine inversă.ExitStackîți permite să faci același lucru dinamic.try/finallynu se compune: ai face nesting, iar ordinea curățărilor devine un puzzle. __exit__rulează mereu. E greu să sari accidental peste curățare cuwith. Cutry/finallypoți uitafinally-ul, sau poți pune codul greșit în afara lui.
try/finally e încă alegerea corectă atunci când curățarea e unică pentru un singur loc și nu merită un nume. Nu face un context manager numit _temp_close_thing care e folosit într-o singură funcție.
Asta a fost despre context managers: instrucțiunea with, protocolul, trei moduri de a-ți scrie propriul context manager și câteva pattern-uri care apar în producție. Modulul următor schimbă viteza: data classes, typing și sistemul modern de tipuri din Python care face ca tot codul pe care l-am scris să fie chiar safe de refactorizat.
Citations (consultat 2026-05-01):
- PEP 343, “The ‘with’ Statement” - https://peps.python.org/pep-0343/
contextlibmodule documentation - https://docs.python.org/3/library/contextlib.html- Python Language Reference, “The with statement” - https://docs.python.org/3/reference/compound_stmts.html#the-with-statement
- PEP 492, “Coroutines with async and await syntax” (async context managers) - https://peps.python.org/pep-0492/