W3docs

L'istruzione with di Python e i Context Manager

Scopri come funziona l'istruzione with di Python e i context manager, come scriverne di propri con __enter__/__exit__ e come usare contextlib.

L'istruzione with garantisce che le risorse come file, connessioni di rete e lock vengano impostate e rilasciate correttamente — anche quando un'eccezione interrompe il blocco. L'oggetto che controlla questa configurazione e il relativo smontaggio si chiama context manager.

Questo capitolo spiega come funziona l'istruzione with, quando usarla, come scrivere i propri context manager usando __enter__ e __exit__, e come crearne di leggeri con contextlib.contextmanager.

Perché esiste with

Prima dell'istruzione with, la gestione delle risorse richiedeva di scrivere blocchi try/finally a mano:

f = open("data.txt", "r", encoding="utf-8")
try:
    content = f.read()
finally:
    f.close()   # must always close, even if read() raises

Questo funziona, ma è verboso, facile da dimenticare e aggiunge codice ripetitivo attorno a ogni risorsa. L'istruzione with condensa tutto in un singolo blocco leggibile e gestisce la pulizia automaticamente:

with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()
# f is closed here, no matter what happened inside the block

La clausola as f lega il valore del context manager al nome f. Alcuni context manager non producono un valore utile; in tal caso è possibile omettere as:

with some_lock:
    shared_data.append(item)

Come funziona l'istruzione with

Quando Python esegue un'istruzione with, segue questa sequenza:

  1. Valuta l'espressione dopo with — questa produce l'oggetto context manager.
  2. Chiama il metodo __enter__() del context manager. Il valore restituito da __enter__() viene assegnato alla variabile as (se presente).
  3. Esegue il corpo del blocco with.
  4. Chiama il metodo __exit__(exc_type, exc_val, exc_tb) del context manager.
    • Se il blocco è terminato normalmente, tutti e tre gli argomenti sono None.
    • Se è stata sollevata un'eccezione, i tre argomenti la descrivono.
    • Se __exit__ restituisce un valore truthy, l'eccezione viene soppressa e l'esecuzione continua dopo il blocco with. Se restituisce un valore falsy (o None), l'eccezione si propaga.

Questo protocollo si chiama protocollo del context manager.

Apertura di file con with

L'uso più comune di with è la gestione dei file. Gli oggetti file built-in di Python implementano il protocollo del context manager, quindi si chiudono automaticamente al termine del blocco:

with open("report.txt", "w", encoding="utf-8") as f:
    f.write("Sales: 1 000\n")
    f.write("Returns: 23\n")

print(f.closed)   # True — file was closed on exit

Se si verifica un'eccezione all'interno del blocco, il file viene comunque chiuso:

try:
    with open("data.txt", "r", encoding="utf-8") as f:
        raise RuntimeError("something went wrong")
except RuntimeError:
    pass

print(f.closed)   # True — closed despite the exception

Senza with, dimenticare f.close() dopo un errore lascia il file descriptor aperto fino all'esecuzione del garbage collector — o fino all'uscita del processo — il che può causare perdita di dati o errori "too many open files" nei programmi a lunga esecuzione.

Apertura di più risorse contemporaneamente

È possibile aprire più risorse in un'unica istruzione with separandole con virgole (Python 3.1+):

with open("input.txt", "r", encoding="utf-8") as src, \
     open("output.txt", "w", encoding="utf-8") as dst:
    for line in src:
        dst.write(line.upper())

Questo equivale esattamente ad annidare due istruzioni with, ma mantiene il livello di indentazione piatto.

Scrivere un context manager con __enter__ e __exit__

Qualsiasi classe che definisce __enter__ e __exit__ può essere usata con l'istruzione with. Ecco un esempio minimale — un timer che misura quanto dura il blocco with:

import time

class Timer:
    def __enter__(self):
        self._start = time.perf_counter()
        return self                        # bound to the 'as' variable

    def __exit__(self, exc_type, exc_val, exc_tb):
        elapsed = time.perf_counter() - self._start
        print(f"Elapsed: {elapsed:.4f}s")
        return False                       # do not suppress exceptions

with Timer() as t:
    total = sum(range(1_000_000))

# Elapsed: 0.0xxx s
print(total)  # 499999500000

Punti chiave:

  • __enter__ viene eseguito prima del blocco. Restituisce il valore assegnato a as t. Restituire self consente al chiamante di accedere a t.elapsed e ad altri attributi se necessario.
  • __exit__ viene eseguito dopo il blocco, anche in caso di eccezione. Restituire False (o None) lascia propagare normalmente qualsiasi eccezione.

Sopprimere eccezioni in __exit__

Se __exit__ restituisce True, l'eccezione viene ingoiata e l'esecuzione continua dopo il blocco with. Questo è intenzionale in contesti specifici — ad esempio, un context manager che cattura e registra gli errori senza far crashare il programma:

class Ignore:
    """Silently ignore any exception raised inside the with block."""

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            print(f"Suppressed: {exc_type.__name__}: {exc_val}")
        return True   # suppress the exception

with Ignore():
    x = 1 / 0        # ZeroDivisionError is caught and ignored

print("execution continues here")
# Suppressed: ZeroDivisionError: division by zero
# execution continues here

Usa la soppressione delle eccezioni con cautela — ingoiare silenziosamente gli errori può nascondere bug. contextlib.suppress della libreria standard è il modo idiomatico per farlo (vedi sotto).

Un context manager per connessioni al database

Un esempio più realistico — gestire una connessione di tipo database che esegue il commit in caso di successo e il rollback in caso di errore:

class ManagedTransaction:
    def __init__(self, connection):
        self.conn = connection

    def __enter__(self):
        self.conn.begin()
        return self.conn

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            self.conn.commit()
        else:
            self.conn.rollback()
        return False   # always let exceptions propagate

Il pattern — commit in caso di successo, rollback in caso di errore — compare in tutte le librerie di database reali (SQLite, SQLAlchemy, psycopg2 lo implementano tutte).

contextlib.contextmanager: Context Manager basati su generatori

Scrivere una classe completa con __enter__ e __exit__ è l'approccio corretto per context manager complessi o con stato. Per i casi più semplici, il decoratore contextlib.contextmanager permette di esprimere la stessa logica come funzione generatrice:

from contextlib import contextmanager

@contextmanager
def managed_open(path, mode="r", encoding="utf-8"):
    print(f"Opening {path}")
    f = open(path, mode, encoding=encoding)
    try:
        yield f          # everything up to yield is __enter__
    finally:
        f.close()        # everything after yield is __exit__
        print(f"Closed {path}")

with managed_open("notes.txt", "w") as f:
    f.write("hello\n")
# Opening notes.txt
# Closed notes.txt

Il protocollo del generatore si mappa direttamente sul protocollo del context manager:

  • Codice prima di yield__enter__ (setup).
  • L'espressione yield → il valore assegnato alla variabile as.
  • Codice dopo yield (di solito in un finally) → __exit__ (teardown).

Il try/finally attorno a yield è importante: senza di esso, un'eccezione all'interno del blocco with causerebbe il mancato eseguimento del codice di teardown.

Esempio con contextmanager: directory di lavoro temporanea

import os
from contextlib import contextmanager

@contextmanager
def working_directory(path):
    original = os.getcwd()
    os.chdir(path)
    try:
        yield
    finally:
        os.chdir(original)

with working_directory("/tmp"):
    print(os.getcwd())   # /tmp (or system temp dir)

print(os.getcwd())       # restored to original directory

Questo pattern è disponibile anche nella libreria standard come tempfile.TemporaryDirectory.

Utilità di contextlib

Il modulo contextlib include diversi context manager pronti all'uso che vale la pena conoscere:

contextlib.suppress

Sopprimi eccezioni specifiche senza alcun codice ripetitivo:

from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove("temp.txt")   # no error even if file does not exist

Equivalente a un try/except che non fa nulla sull'eccezione catturata.

contextlib.nullcontext

Un context manager no-op utile quando si vuole condizionalmente usare un context manager oppure no:

from contextlib import nullcontext

def process(data, lock=None):
    ctx = lock if lock is not None else nullcontext()
    with ctx:
        return sorted(data)

Senza nullcontext, sarrebbe necessario un ramo if lock: ogni volta.

contextlib.ExitStack

ExitStack permette di gestire un numero dinamico di context manager — utile quando il numero di risorse non è noto fino al runtime:

from contextlib import ExitStack

files = ["a.txt", "b.txt", "c.txt"]

with ExitStack() as stack:
    handles = [
        stack.enter_context(open(f, "w", encoding="utf-8"))
        for f in files
    ]
    for i, fh in enumerate(handles):
        fh.write(f"file {i}\n")
# All three files are closed here

ExitStack è anche lo strumento giusto quando è necessario aggiungere condizionalmente un context manager, o quando si vuole rimandare la pulizia a un momento successivo.

Quando usare with vs. Try/Finally

Usa with ogni volta che:

  • Una risorsa deve essere rilasciata dopo l'uso (file, socket, lock, cursori di database).
  • Vuoi garantire la pulizia anche in caso di eccezioni.
  • La logica di pulizia è sempre la stessa indipendentemente dal successo o dal fallimento.

Usa un semplice try/finally solo quando:

  • Hai bisogno di azioni di pulizia diverse a seconda del tipo di eccezione — anche se __exit__ può farlo ugualmente.
  • Stai scrivendo codice compatibile con Python 2 (raro oggigiorno).

In pratica, se l'oggetto supporta il protocollo del context manager, preferisci sempre with.

Riferimento rapido

FunzionalitàCosa fa
with expr as v:Chiama expr.__enter__(), assegna il risultato a v, chiama __exit__ all'uscita
Risorse multiplewith A() as a, B() as b: — entrambe pulite anche se B() solleva un'eccezione
__enter__(self)Setup; il valore restituito è assegnato alla variabile as
__exit__(self, exc_type, exc_val, exc_tb)Teardown; restituisce True per sopprimere l'eccezione
@contextmanagerTrasforma una funzione generatrice in un context manager
contextlib.suppress(E)Inghiotte il tipo di eccezione E senza un try/except
contextlib.nullcontext()Segnaposto quando un context manager è facoltativo
contextlib.ExitStackGestisce un insieme dinamico o condizionale di context manager

Capitoli correlati

Esercizi

Pratica
What method does a context manager call when the with block is entered?
What method does a context manager call when the with block is entered?
Pratica
What happens when __exit__ returns True?
What happens when __exit__ returns True?
Pratica
In a @contextmanager generator, code before the yield statement corresponds to which part of the context manager protocol?
In a @contextmanager generator, code before the yield statement corresponds to which part of the context manager protocol?
Pratica
Which contextlib utility suppresses specific exceptions without a try/except block?
Which contextlib utility suppresses specific exceptions without a try/except block?
Was this page helpful?