W3docs

L'instruction with et les gestionnaires de contexte en Python

Apprenez comment fonctionnent l'instruction with et les gestionnaires de contexte en Python, comment créer les vôtres avec __enter__/__exit__ et contextlib.

L'instruction with garantit que les ressources telles que les fichiers, les connexions réseau et les verrous sont correctement initialisées et libérées — même lorsqu'une exception interrompt le bloc. L'objet qui contrôle cette initialisation et ce nettoyage s'appelle un gestionnaire de contexte.

Ce chapitre explique comment fonctionne l'instruction with, quand l'utiliser, comment écrire vos propres gestionnaires de contexte avec __enter__ et __exit__, et comment en créer des plus légers avec contextlib.contextmanager.

Pourquoi with existe

Avant l'instruction with, la gestion des ressources impliquait d'écrire des blocs try/finally manuellement :

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

Cela fonctionne, mais c'est verbeux, facile à oublier, et ajoute du code répétitif autour de chaque ressource. L'instruction with condense tout cela en un seul bloc lisible et gère le nettoyage automatiquement :

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 clause as f lie la valeur du gestionnaire de contexte au nom f. Certains gestionnaires de contexte ne produisent pas de valeur utile, auquel cas vous pouvez omettre as :

with some_lock:
    shared_data.append(item)

Comment fonctionne l'instruction with

Lorsque Python exécute une instruction with, il suit cette séquence :

  1. Évaluer l'expression après with — cela produit l'objet gestionnaire de contexte.
  2. Appeler la méthode __enter__() du gestionnaire de contexte. La valeur de retour de __enter__() est liée à la variable as (si présente).
  3. Exécuter le corps du bloc with.
  4. Appeler la méthode __exit__(exc_type, exc_val, exc_tb) du gestionnaire de contexte.
    • Si le bloc s'est terminé normalement, les trois arguments valent None.
    • Si une exception a été levée, les trois arguments la décrivent.
    • Si __exit__ retourne une valeur vraie, l'exception est supprimée et l'exécution continue après le bloc with. Si elle retourne une valeur fausse (ou None), l'exception se propage.

Ce protocole s'appelle le protocole de gestionnaire de contexte.

Ouverture de fichiers avec with

L'usage le plus courant de with est la gestion de fichiers. Les objets fichier intégrés de Python implémentent le protocole de gestionnaire de contexte, ils se ferment donc automatiquement à la fin du bloc :

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

Si une exception se produit à l'intérieur du bloc, le fichier est quand même fermé :

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

Sans with, oublier f.close() après une erreur laisse le descripteur de fichier ouvert jusqu'à ce que le ramasse-miettes s'exécute — ou jusqu'à ce que le processus se termine — ce qui peut provoquer des pertes de données ou des erreurs « too many open files » dans les programmes de longue durée.

Ouverture de plusieurs ressources à la fois

Vous pouvez ouvrir plusieurs ressources dans une seule instruction with en les séparant par des virgules (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())

C'est exactement équivalent à imbriquer deux instructions with, mais cela maintient le niveau d'indentation à plat.

Écrire un gestionnaire de contexte avec __enter__ et __exit__

Toute classe qui définit __enter__ et __exit__ peut être utilisée avec l'instruction with. Voici un exemple minimal — un minuteur qui mesure la durée d'exécution du bloc 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

Points clés :

  • __enter__ s'exécute avant le bloc. Il retourne la valeur liée à as t. Retourner self permet à l'appelant d'accéder à t.elapsed et à d'autres attributs si nécessaire.
  • __exit__ s'exécute après le bloc, même en cas d'exception. Retourner False (ou None) laisse toute exception se propager normalement.

Suppression des exceptions dans __exit__

Si __exit__ retourne True, l'exception est avalée et l'exécution continue après le bloc with. C'est intentionnel dans des contextes spécifiques — par exemple, un gestionnaire de contexte qui capture et journalise les erreurs sans faire planter le programme :

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

Utilisez la suppression des exceptions avec précaution — avaler silencieusement des erreurs peut masquer des bugs. Le contextlib.suppress de la bibliothèque standard est la manière idiomatique de faire cela (voir ci-dessous).

Un gestionnaire de contexte pour connexion de base de données

Un exemple plus réaliste — gérer une connexion de type base de données qui valide en cas de succès et effectue un rollback en cas d'erreur :

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

Le motif — valider en cas de succès, effectuer un rollback en cas d'échec — apparaît dans toutes les vraies bibliothèques de bases de données (SQLite, SQLAlchemy, psycopg2 l'implémentent toutes).

contextlib.contextmanager : gestionnaires de contexte basés sur des générateurs

Écrire une classe complète avec __enter__ et __exit__ est la bonne approche pour les gestionnaires de contexte complexes ou à état. Pour les cas plus simples, le décorateur contextlib.contextmanager vous permet d'exprimer la même logique sous forme de fonction génératrice :

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

Le protocole de générateur se mappe directement sur le protocole de gestionnaire de contexte :

  • Le code avant yield__enter__ (initialisation).
  • L'expression yield → la valeur liée à la variable as.
  • Le code après yield (généralement dans un finally) → __exit__ (nettoyage).

Le try/finally autour de yield est important : sans lui, une exception à l'intérieur du bloc with empêcherait l'exécution du code de nettoyage.

Exemple de contextmanager : répertoire de travail temporaire

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

Ce motif est également disponible dans la bibliothèque standard sous la forme tempfile.TemporaryDirectory.

Utilitaires de contextlib

Le module contextlib fournit plusieurs gestionnaires de contexte prêts à l'emploi qu'il vaut la peine de connaître :

contextlib.suppress

Supprime des exceptions spécifiques sans aucun code répétitif :

from contextlib import suppress

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

Équivalent à un try/except qui ne fait rien sur l'exception capturée.

contextlib.nullcontext

Un gestionnaire de contexte sans opération, utile lorsque vous voulez conditionnellement utiliser un gestionnaire de contexte ou non :

from contextlib import nullcontext

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

Sans nullcontext, vous auriez besoin d'une branche if lock: à chaque fois.

contextlib.ExitStack

ExitStack vous permet de gérer un nombre dynamique de gestionnaires de contexte — utile lorsque le nombre de ressources n'est pas connu avant l'exécution :

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 est également l'outil approprié lorsque vous devez ajouter conditionnellement un gestionnaire de contexte, ou lorsque vous voulez différer le nettoyage à un moment ultérieur.

Quand utiliser with plutôt que Try/Finally

Utilisez with chaque fois que :

  • Une ressource doit être libérée après utilisation (fichiers, sockets, verrous, curseurs de base de données).
  • Vous voulez garantir le nettoyage même en cas d'exception.
  • La logique de nettoyage est toujours la même qu'il y ait succès ou échec.

Utilisez un simple try/finally seulement lorsque :

  • Vous avez besoin d'actions de nettoyage différentes selon le type d'exception — bien que __exit__ puisse le faire aussi.
  • Vous écrivez du code compatible Python 2 (rare aujourd'hui).

En pratique, si l'objet supporte le protocole de gestionnaire de contexte, préférez toujours with.

Référence rapide

FonctionnalitéCe qu'elle fait
with expr as v:Appelle expr.__enter__(), lie le résultat à v, appelle __exit__ à la sortie
Ressources multipleswith A() as a, B() as b: — toutes deux nettoyées même si B() lève une exception
__enter__(self)Initialisation ; la valeur de retour est liée à la variable as
__exit__(self, exc_type, exc_val, exc_tb)Nettoyage ; retourner True pour supprimer l'exception
@contextmanagerTransforme une fonction génératrice en gestionnaire de contexte
contextlib.suppress(E)Avale le type d'exception E sans try/except
contextlib.nullcontext()Espace réservé lorsqu'un gestionnaire de contexte est optionnel
contextlib.ExitStackGère un ensemble dynamique ou conditionnel de gestionnaires de contexte

Chapitres connexes

Pratique

Pratique
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?
Pratique
What happens when __exit__ returns True?
What happens when __exit__ returns True?
Pratique
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?
Pratique
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?