W3docs

La sentencia with de Python y los gestores de contexto

Aprende cómo funcionan la sentencia with y los gestores de contexto en Python, cómo escribir los tuyos con __enter__/__exit__ y cómo usar contextlib.

La sentencia with garantiza que los recursos como archivos, conexiones de red y bloqueos se configuren y limpien correctamente, incluso cuando una excepción interrumpe el bloque. El objeto que controla esta configuración y desmontaje se llama gestor de contexto.

Este capítulo explica cómo funciona la sentencia with, cuándo usarla, cómo escribir tus propios gestores de contexto con __enter__ y __exit__, y cómo crear versiones ligeras con contextlib.contextmanager.

Por qué existe with

Antes de la sentencia with, la gestión de recursos implicaba escribir bloques 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

Esto funciona, pero es verboso, fácil de olvidar y añade código repetitivo alrededor de cada recurso. La sentencia with condensa esto en un bloque único y legible, y gestiona la limpieza automáticamente:

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 cláusula as f enlaza el valor del gestor de contexto al nombre f. Algunos gestores de contexto no producen un valor útil; en ese caso puedes omitir as:

with some_lock:
    shared_data.append(item)

Cómo funciona la sentencia with

Cuando Python ejecuta una sentencia with, sigue esta secuencia:

  1. Evalúa la expresión después de with — esto produce el objeto gestor de contexto.
  2. Llama al método __enter__() del gestor de contexto. El valor de retorno de __enter__() se enlaza a la variable as (si está presente).
  3. Ejecuta el cuerpo del bloque with.
  4. Llama al método __exit__(exc_type, exc_val, exc_tb) del gestor de contexto.
    • Si el bloque se completó normalmente, los tres argumentos son None.
    • Si se lanzó una excepción, los tres argumentos la describen.
    • Si __exit__ devuelve un valor verdadero, la excepción se suprime y la ejecución continúa después del bloque with. Si devuelve un valor falso (o None), la excepción se propaga.

Este protocolo se denomina el protocolo de gestor de contexto.

Abrir archivos con with

El uso más común de with es el manejo de archivos. Los objetos de archivo integrados de Python implementan el protocolo de gestor de contexto, por lo que se cierran automáticamente cuando termina el bloque:

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 ocurre una excepción dentro del bloque, el archivo sigue cerrándose:

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

Sin with, olvidar f.close() después de un error deja el descriptor de archivo abierto hasta que se ejecute el recolector de basura, o hasta que el proceso termine, lo que puede causar pérdida de datos o errores de "demasiados archivos abiertos" en programas de larga duración.

Abrir múltiples recursos a la vez

Puedes abrir varios recursos en una sola sentencia with separándolos con comas (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())

Esto es exactamente equivalente a anidar dos sentencias with, pero mantiene el nivel de sangría plano.

Escribir un gestor de contexto con __enter__ y __exit__

Cualquier clase que defina __enter__ y __exit__ puede usarse con la sentencia with. Aquí hay un ejemplo mínimo — un temporizador que mide cuánto tiempo tarda en ejecutarse el bloque 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

Puntos clave:

  • __enter__ se ejecuta antes del bloque. Devuelve el valor enlazado a as t. Devolver self permite al llamador acceder a t.elapsed y otros atributos si es necesario.
  • __exit__ se ejecuta después del bloque, incluso en caso de excepción. Devolver False (o None) permite que cualquier excepción se propague normalmente.

Suprimir excepciones en __exit__

Si __exit__ devuelve True, la excepción se silencia y la ejecución continúa después del bloque with. Esto es intencional en contextos específicos — por ejemplo, un gestor de contexto que captura y registra errores sin interrumpir el programa:

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 supresión de excepciones con cuidado — silenciar errores puede ocultar errores en el código. contextlib.suppress de la biblioteca estándar es la forma idiomática de hacer esto (véase más abajo).

Un gestor de contexto para conexiones de base de datos

Un ejemplo más realista — gestionar una conexión estilo base de datos que confirma en caso de éxito y revierte en caso de error:

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

El patrón — confirmar en éxito, revertir en fallo — aparece en las bibliotecas de bases de datos reales (SQLite, SQLAlchemy y psycopg2 lo implementan todos).

contextlib.contextmanager: gestores de contexto basados en generadores

Escribir una clase completa con __enter__ y __exit__ es el enfoque correcto para gestores de contexto complejos o con estado. Para casos más simples, el decorador contextlib.contextmanager permite expresar la misma lógica como una función generadora:

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

El protocolo del generador se mapea directamente sobre el protocolo del gestor de contexto:

  • El código antes de yield__enter__ (configuración).
  • La expresión yield → el valor enlazado a la variable as.
  • El código después de yield (normalmente en un finally) → __exit__ (desmontaje).

El try/finally alrededor de yield es importante: sin él, una excepción dentro del bloque with haría que el código de desmontaje nunca se ejecutara.

Ejemplo de contextmanager: directorio de trabajo temporal

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

Este patrón también está disponible en la biblioteca estándar como tempfile.TemporaryDirectory.

Utilidades de contextlib

El módulo contextlib incluye varios gestores de contexto predefinidos que vale la pena conocer:

contextlib.suppress

Suprime excepciones específicas sin ningún código repetitivo:

from contextlib import suppress

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

Equivalente a un try/except que no hace nada cuando captura la excepción.

contextlib.nullcontext

Un gestor de contexto sin operación, útil cuando condicionalmente quieres usar un gestor de contexto o no:

from contextlib import nullcontext

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

Sin nullcontext, necesitarías una rama if lock: cada vez.

contextlib.ExitStack

ExitStack permite gestionar un número dinámico de gestores de contexto — útil cuando el número de recursos no se conoce hasta el momento de ejecución:

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 también es la herramienta adecuada cuando necesitas añadir condicionalmente un gestor de contexto, o cuando quieres diferir la limpieza a un punto posterior.

Cuándo usar with frente a try/finally

Usa with siempre que:

  • Un recurso deba liberarse después de su uso (archivos, sockets, bloqueos, cursores de base de datos).
  • Quieras garantizar la limpieza incluso en caso de excepciones.
  • La lógica de limpieza sea siempre la misma independientemente del éxito o el fracaso.

Usa un try/finally simple solo cuando:

  • Necesites acciones de limpieza diferentes según el tipo de excepción — aunque __exit__ también puede hacer esto.
  • Estés escribiendo código compatible con Python 2 (poco frecuente hoy en día).

En la práctica, si el objeto admite el protocolo de gestor de contexto, siempre prefiere with.

Referencia rápida

CaracterísticaQué hace
with expr as v:Llama a expr.__enter__(), enlaza el resultado a v, llama a __exit__ al salir
Múltiples recursoswith A() as a, B() as b: — ambos se limpian aunque B() lance una excepción
__enter__(self)Configuración; el valor de retorno se enlaza a la variable as
__exit__(self, exc_type, exc_val, exc_tb)Desmontaje; devuelve True para suprimir la excepción
@contextmanagerConvierte una función generadora en un gestor de contexto
contextlib.suppress(E)Silencia el tipo de excepción E sin un try/except
contextlib.nullcontext()Marcador de posición cuando un gestor de contexto es opcional
contextlib.ExitStackGestiona un conjunto dinámico o condicional de gestores de contexto

Capítulos relacionados

Práctica

Práctica
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?
Práctica
What happens when __exit__ returns True?
What happens when __exit__ returns True?
Práctica
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?
Práctica
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?