Qu'est-ce que l'instruction with en Python ?

Découvrez l'instruction with en Python : gestion automatique des ressources, context managers, protocole __enter__ et __exit__, exemples pratiques et bonnes pratiques.
10 min de lecture
Believemy logo

Définition de l'instruction with en Python

L'instruction with en Python est un mécanisme puissant qui permet de gérer automatiquement les ressources telles que les fichiers, les connexions réseau ou les verrous. Elle garantit qu'une ressource est correctement initialisée avant utilisation et proprement libérée après usage, même si une erreur survient pendant l'exécution du bloc de code.

Concrètement, with repose sur le concept de context manager (gestionnaire de contexte). Lorsque vous utilisez cette instruction, Python appelle automatiquement deux méthodes spéciales : __enter__ à l'entrée du bloc et __exit__ à la sortie, que celle-ci soit normale ou causée par une exception. Ce mécanisme rend votre code plus sûr, plus lisible et plus concis.

Si vous souhaitez maîtriser Python en profondeur, y compris les concepts avancés comme les context managers, nous vous recommandons de suivre notre formation complète sur Python disponible sur Believemy.


Syntaxe de base de with

La syntaxe de l'instruction with est simple et élégante :

PYTHON
with expression as variable:
    # bloc de code utilisant la variable
    pass

Voici comment cela fonctionne en détail :

  • expression : une expression qui retourne un objet supportant le protocole de context manager (c'est-à-dire possédant les méthodes __enter__ et __exit__).
  • as variable : (optionnel) assigne la valeur retournée par __enter__ à une variable que vous pouvez utiliser dans le bloc.
  • Le bloc indenté : le code qui s'exécute dans le contexte géré.

À la fin du bloc (ou en cas d'exception), la méthode __exit__ est automatiquement appelée, assurant la libération propre de la ressource.


Pourquoi utiliser with plutôt que try/finally ?

Avant l'introduction de with (Python 2.5), la gestion des ressources passait systématiquement par des blocs try/finally. Comparons les deux approches :

Sans with (approche classique)

PYTHON
fichier = open("donnees.txt", "r")
try:
    contenu = fichier.read()
    print(contenu)
finally:
    fichier.close()


Avec with (approche moderne)

PYTHON
with open("donnees.txt", "r") as fichier:
    contenu = fichier.read()
    print(contenu)

La différence est frappante. Avec with, vous n'avez plus besoin de penser à fermer le fichier manuellement. Le fichier est automatiquement fermé à la sortie du bloc, même si une exception est levée. Le code est plus court, plus lisible et moins sujet aux erreurs.

Bon à savoir

L'instruction with n'est pas limitée aux fichiers. Elle fonctionne avec tout objet implémentant le protocole de context manager, comme les connexions de base de données, les verrous de threads, les transactions, etc.


Exemples pratiques

Lecture et écriture de fichiers

L'utilisation la plus courante de with est la manipulation de fichiers. Voici comment lire et écrire dans des fichiers en toute sécurité :

PYTHON
# Écriture dans un fichier
with open("journal.txt", "w") as fichier:
    fichier.write("Première ligne\n")
    fichier.write("Deuxième ligne\n")
    fichier.write("Troisième ligne\n")

# Lecture du fichier
with open("journal.txt", "r") as fichier:
    for ligne in fichier:
        print(ligne.strip())

Dans cet exemple, même si une erreur se produit pendant l'écriture ou la lecture, le fichier sera correctement fermé grâce à with.


Gestion de plusieurs ressources simultanément

Depuis Python 3.1, vous pouvez gérer plusieurs context managers dans une seule instruction with :

PYTHON
# Copier le contenu d'un fichier dans un autre
with open("source.txt", "r") as source, open("destination.txt", "w") as destination:
    for ligne in source:
        destination.write(ligne.upper())

Cette syntaxe est équivalente à imbriquer deux blocs with, mais elle est beaucoup plus compacte :

PYTHON
# Équivalent avec des blocs imbriqués
with open("source.txt", "r") as source:
    with open("destination.txt", "w") as destination:
        for ligne in source:
            destination.write(ligne.upper())


Utilisation avec des verrous (threading)

L'instruction with est particulièrement utile dans la programmation concurrente pour gérer les verrous de manière sûre :

PYTHON
import threading

verrou = threading.Lock()
compteur = 0

def incrementer():
    global compteur
    with verrou:
        # Le verrou est automatiquement acquis ici
        valeur_actuelle = compteur
        compteur = valeur_actuelle + 1
    # Le verrou est automatiquement relâché ici

# Créer et lancer plusieurs threads
threads = [threading.Thread(target=incrementer) for _ in range(100)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(f"Compteur final : {compteur}")  # Affiche : Compteur final : 100

Remarquez que dans le cas des verrous, la clause as n'est pas nécessaire car nous n'avons pas besoin d'une variable de retour.


Créer vos propres context managers

Vous pouvez créer vos propres context managers de deux façons : en utilisant une class avec les méthodes spéciales, ou en utilisant le décorateur contextlib.contextmanager.

Méthode 1 : avec une classe

Pour créer un context manager à partir d'une classe, vous devez implémenter les méthodes __enter__ et __exit__ :

PYTHON
class Chronometre:
    """Context manager pour mesurer le temps d'exécution d'un bloc de code."""
    
    def __init__(self, nom="Bloc"):
        self.nom = nom
        self.debut = None
    
    def __enter__(self):
        import time
        self.debut = time.time()
        print(f"⏱️ Début de la mesure pour '{self.nom}'")
        return self  # Valeur assignée à la variable après 'as'
    
    def __exit__(self, type_exc, valeur_exc, traceback_exc):
        import time
        duree = time.time() - self.debut
        print(f"⏱️ '{self.nom}' terminé en {duree:.4f} secondes")
        return False  # Ne pas supprimer les exceptions


# Utilisation
with Chronometre("Calcul intensif") as chrono:
    total = sum(range(1_000_000))
    print(f"Résultat : {total}")

La méthode __exit__ reçoit trois paramètres relatifs à une éventuelle exception :

  • type_exc : le type de l'exception (ou None si aucune exception).
  • valeur_exc : la valeur de l'exception.
  • traceback_exc : le traceback de l'exception.

Si __exit__ retourne True, l'exception est supprimée. Si elle retourne False (ou None), l'exception est propagée normalement.


Méthode 2 : avec contextlib et un générateur

Le module contextlib offre un décorateur qui permet de créer un context manager à partir d'une simple fonction génératrice utilisant yield :

PYTHON
from contextlib import contextmanager

@contextmanager
def repertoire_temporaire(prefixe="tmp"):
    import tempfile
    import shutil
    
    # Code d'entrée (__enter__)
    chemin = tempfile.mkdtemp(prefix=prefixe)
    print(f"📁 Répertoire temporaire créé : {chemin}")
    
    try:
        yield chemin  # Valeur assignée à la variable après 'as'
    finally:
        # Code de sortie (__exit__)
        shutil.rmtree(chemin)
        print(f"🗑️ Répertoire temporaire supprimé : {chemin}")


# Utilisation
with repertoire_temporaire("mon_app_") as dossier:
    print(f"Travail dans : {dossier}")
    # Le dossier existe ici
# Le dossier est automatiquement supprimé ici
Attention

Lorsque vous utilisez @contextmanager, placez toujours le yield dans un bloc try/finally pour garantir que le code de nettoyage s'exécute même en cas d'exception.


Le protocole de context manager en détail

Pour mieux comprendre comment with fonctionne en interne, voici le déroulement précis de l'exécution :

ÉtapeActionDescription
1Évaluation de l'expressionL'expression après with est évaluée pour obtenir le context manager
2Appel de __enter__La méthode est appelée et sa valeur de retour est assignée à la variable après as
3Exécution du blocLe code indenté sous with est exécuté
4Appel de __exit__La méthode est appelée, que le bloc se termine normalement ou par une exception
5Gestion d'exceptionSi __exit__ retourne True, l'exception est supprimée ; sinon, elle est propagée


Context managers intégrés courants

Python propose de nombreux context managers intégrés que vous pouvez utiliser directement avec with :

Suppression d'exceptions avec contextlib.suppress

PYTHON
from contextlib import suppress

# Ignorer une erreur spécifique
with suppress(FileNotFoundError):
    import os
    os.remove("fichier_inexistant.txt")
    # Pas d'erreur levée si le fichier n'existe pas

print("Le programme continue normalement")


Redirection de la sortie standard

PYTHON
from contextlib import redirect_stdout
import io

# Capturer la sortie de print dans une chaîne
tampon = io.StringIO()
with redirect_stdout(tampon):
    print("Ce texte est capturé")
    print("Celui-ci aussi")

resultat = tampon.getvalue()
print(f"Texte capturé : {resultat}")  # Affiche le texte capturé


Changement de répertoire temporaire

PYTHON
import os
from contextlib import contextmanager

@contextmanager
def changer_repertoire(chemin):
    ancien = os.getcwd()
    os.chdir(chemin)
    try:
        yield
    finally:
        os.chdir(ancien)

# Utilisation
print(f"Répertoire actuel : {os.getcwd()}")
with changer_repertoire("/tmp"):
    print(f"Répertoire temporaire : {os.getcwd()}")
print(f"Retour au répertoire : {os.getcwd()}")


Context managers asynchrones (async with)

Depuis Python 3.5, il est possible d'utiliser with dans un contexte asynchrone grâce à async with. Les méthodes correspondantes sont __aenter__ et __aexit__ :

PYTHON
import asyncio

class SessionHTTP:
    """Exemple simplifié de context manager asynchrone."""
    
    async def __aenter__(self):
        print("🌐 Ouverture de la session HTTP")
        # Simule l'ouverture d'une connexion
        await asyncio.sleep(0.1)
        return self
    
    async def __aexit__(self, type_exc, valeur_exc, traceback_exc):
        print("🌐 Fermeture de la session HTTP")
        # Simule la fermeture de la connexion
        await asyncio.sleep(0.1)
        return False
    
    async def get(self, url):
        print(f"📡 Requête GET vers {url}")
        await asyncio.sleep(0.2)
        return {"status": 200, "data": "OK"}


async def main():
    async with SessionHTTP() as session:
        reponse = await session.get("https://api.exemple.com/donnees")
        print(f"Réponse : {reponse}")

asyncio.run(main())


Bonnes pratiques

Voici les bonnes pratiques à suivre lorsque vous utilisez l'instruction with en Python :

  • Préférez toujours with pour les fichiers : n'utilisez jamais open() sans with. C'est la manière recommandée par la communauté Python et la documentation officielle.
  • Utilisez with pour toute ressource nécessitant un nettoyage : connexions de base de données, sockets réseau, verrous, fichiers temporaires, etc.
  • Gardez les blocs with courts : libérez les ressources le plus tôt possible en n'incluant dans le bloc que le code qui a réellement besoin de la ressource.
  • Préférez @contextmanager pour les cas simples : si votre context manager est simple, le décorateur est plus concis qu'une class complète.
  • N'oubliez pas le try/finally dans @contextmanager : sans cela, le code de nettoyage ne s'exécutera pas en cas d'exception.
  • Documentez vos context managers : utilisez des docstring pour expliquer ce que fait votre context manager, ce qu'il retourne et les exceptions qu'il peut gérer.
  • Retournez False dans __exit__ par défaut : ne supprimez les exceptions que si vous avez une très bonne raison de le faire.
Bon à savoir

Un bon réflexe : chaque fois que vous écrivez un bloc try/finally pour libérer une ressource, demandez-vous si un context manager ne serait pas plus approprié. La réponse est presque toujours oui.


Questions fréquentes

Question

Quelle est la différence entre with et try/finally en Python ?

Les deux approches permettent de garantir l'exécution d'un code de nettoyage, mais with est plus concis et moins sujet aux erreurs. Avec try/finally, vous devez gérer manuellement l'acquisition et la libération de la ressource, ce qui peut mener à des oublis. L'instruction with encapsule cette logique dans un objet réutilisable (le context manager), rendant le code plus propre et plus maintenable. De plus, le context manager peut être partagé entre plusieurs parties de votre code, favorisant ainsi la réutilisation.


Question

Peut-on utiliser with sans la clause as ?

Oui, la clause as est optionnelle. Elle n'est utile que lorsque vous avez besoin de la valeur retournée par __enter__. Par exemple, avec un verrou (threading.Lock), vous n'avez généralement pas besoin de la variable : with verrou: suffit. En revanche, avec open(), la clause as est indispensable pour accéder à l'objet fichier.


Question

Peut-on imbriquer plusieurs instructions with ?

Oui, vous pouvez imbriquer des blocs with ou utiliser la syntaxe compacte avec des virgules : with open("a.txt") as a, open("b.txt") as b:. Depuis Python 3.10, vous pouvez également utiliser des parenthèses pour une meilleure lisibilité sur plusieurs lignes : with (open("a.txt") as a, open("b.txt") as b):. Les deux approches sont équivalentes fonctionnellement.


Question

Comment apprendre à maîtriser l'instruction with et Python en général ?

L'instruction with fait partie des fonctionnalités essentielles de Python qui distinguent un code amateur d'un code professionnel. Pour maîtriser cette instruction et l'ensemble du langage Python, nous vous recommandons de suivre notre formation Python sur Believemy. Vous y apprendrez non seulement les bases du langage, mais aussi les concepts avancés comme les context managers, la programmation asynchrone et les bonnes pratiques de développement.

Termes connexes

Découvrez notre glossaire Python

Parcourez les termes et définitions les plus couramment utilisés dans le domaine du développement avec Python.

Partager cet article

Tu veux nous aider ? Fais un lien vers cet article sur tes réseaux ou encore mieux : sur ton site, dans un article ou dans ta newsletter.