Оператор with в Python выполняет блок кода внутри контекстного менеджера: перед входом в блок вызывает у менеджера метод __enter__, а после выхода — метод __exit__. Выход засчитывается любой: обычное завершение, return, break или исключение. Поэтому with применяют там, где ресурс нужно гарантированно освободить: закрыть файл, отпустить блокировку, завершить транзакцию.
Содержание
- Три термина, которые легко спутать
- Минимальный пример: with и файл
- Что with делает по шагам
- Что возвращает exit и как подавляются исключения
- contextlib: менеджер без класса
- Несколько менеджеров в одном with
- async with: асинхронные менеджеры
- Менеджер делает не всегда то, что вы ждете
- Если не получилось: типовые симптомы
- Выводы
- Где применяется / связь с практикой
- FAQ
Примеры проверены на Python 3.14.7 (официальный образ python:3.14-slim), 24.09.2026. Ниже разберем протокол по шагам, что возвращает __exit__, как писать свои менеджеры через contextlib, async with и запись нескольких менеджеров в скобках.
Три термина, которые легко спутать
| Термин | Что это | Пример |
|---|---|---|
| Оператор with | Синтаксическая конструкция языка | with open(...) as f: |
| Контекстный менеджер | Объект с методами __enter__ и __exit__ |
файловый объект, threading.Lock |
| Цель as | То, что вернул __enter__, а не обязательно сам менеджер |
для файла — сам файл, для Lock — True |
Последняя строка — частый источник путаницы. У open() метод __enter__ возвращает тот же файловый объект, поэтому кажется, что as просто дает имя менеджеру. Но у threading.Lock в as попадет True, а у своего класса — что угодно.
Минимальный пример: with и файл
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("привет\n")
print(f.closed) # True: файл закрыт сразу после выхода из блока
Программа напечатает True. Переменная f после блока по-прежнему существует (with не создает своей области видимости), но файл уже закрыт. Подробно про режимы открытия, кодировки и чтение файлов — в статье «Python: основы работы с файлами»; здесь нас интересует сам механизм with.
Что with делает по шагам
Упрощенно запись with EXPR as VAR: BLOCK разворачивается в такую схему (полная версия — в PEP 343):
import sys
manager = EXPR
enter = type(manager).__enter__
exit_ = type(manager).__exit__
VAR = enter(manager)
hit_except = False
try:
BLOCK
except BaseException:
hit_except = True
if not exit_(manager, *sys.exc_info()):
raise # __exit__ вернул ложь - исключение летит дальше
finally:
if not hit_except: # обычный выход, return, break, continue
exit_(manager, None, None, None)
Этот фрагмент — схема, а не готовый к запуску код (EXPR, VAR, BLOCK — подстановки). Нормальный выход обрабатывается в finally, а не в else: так __exit__ вызывается и при return, break или continue внутри блока. Из схемы видно три вещи:
- Методы ищутся у типа объекта, а не у экземпляра. Если присвоить
obj.__enter__ = ...конкретному объекту, with этот атрибут не увидит. __exit__получает три аргумента: тип исключения, само исключение и трассировку. При нормальном выходе все три равныNone.- Если
__enter__упал,__exit__не вызывается: ресурс считается не захваченным.
Проверим на своем классе:
class Tracer:
def __init__(self, name):
self.name = name
def __enter__(self):
print(f"вход в {self.name}")
return self.name.upper() # это значение попадет в as
def __exit__(self, exc_type, exc, tb):
print(f"выход из {self.name}, исключение: {exc_type}")
return False # не подавлять исключение
with Tracer("блок") as value:
print("внутри, value =", value)
try:
with Tracer("сбой"):
raise ValueError("что-то пошло не так")
except ValueError as e:
print("поймано снаружи:", e)
Вывод:
вход в блок
внутри, value = БЛОК
выход из блок, исключение: None
вход в сбой
выход из сбой, исключение: <class 'ValueError'>
поймано снаружи: что-то пошло не так
В value лежит строка БЛОК — результат __enter__, а не объект Tracer. При исключении __exit__ сначала отработал, и только потом ошибка дошла до внешнего except.
Что возвращает exit и как подавляются исключения
Правило одно: если __exit__ вернул истинное значение, исключение из блока подавляется, и программа продолжает работу после with. Ложное значение (False, None или отсутствие return) — исключение пробрасывается дальше. При нормальном выходе возвращаемое значение ни на что не влияет.
Типичная ошибка — вернуть True «на всякий случай»:
class Swallow:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
print("закрываю ресурс")
return True # ошибка: глушит ВСЕ исключения
with Swallow():
result = 1 / 0
print("программа продолжила работу, result не создан:", "result" in globals())
закрываю ресурс
программа продолжила работу, result не создан: False
Деление на ноль бесследно исчезло, а переменная result так и не появилась — дальше код упадет в неожиданном месте. Исправление: подавлять только то исключение, которое вы действительно ожидаете.
class IgnoreMissing:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
print("закрываю ресурс")
return exc_type is not None and issubclass(exc_type, FileNotFoundError)
with IgnoreMissing():
open("no-such-file.txt")
print("FileNotFoundError подавлен")
with IgnoreMissing():
1 / 0
закрываю ресурс
FileNotFoundError подавлен
закрываю ресурс
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
Отсутствующий файл подавлен, а деление на ноль, как и положено, дошло до пользователя.
contextlib: менеджер без класса
Писать класс ради пары действий «до» и «после» не обязательно. Модуль contextlib из стандартной библиотеки дает готовые инструменты.
@contextmanager и обязательный try/finally
Декоратор contextmanager превращает генератор в менеджер: код до yield — это __enter__, значение yield попадает в as, код после — это __exit__. Исключение из блока with возникает в точке yield, поэтому без try/finally очистка не выполнится:
from contextlib import contextmanager
@contextmanager
def broken():
print("захватили ресурс")
yield
print("освободили ресурс") # не выполнится при исключении
@contextmanager
def fixed():
print("захватили ресурс")
try:
yield
finally:
print("освободили ресурс")
for cm in (broken, fixed):
try:
with cm():
raise KeyError("x")
except KeyError:
print(f"{cm.__name__}: исключение дошло наружу\n")
захватили ресурс
broken: исключение дошло наружу
захватили ресурс
освободили ресурс
fixed: исключение дошло наружу
В варианте broken строки «освободили ресурс» нет: ресурс утек. Если внутри генератора перехватить исключение через except и не выбросить его снова, оно будет подавлено — аналог return True в __exit__.
suppress и ExitStack
suppress(*exceptions) подавляет перечисленные исключения и заменяет конструкцию try/except: pass. ExitStack нужен, когда число ресурсов заранее неизвестно: каждый добавленный менеджер закроется при выходе, в обратном порядке.
import os
import tempfile
from contextlib import ExitStack, suppress
tmp = tempfile.mkdtemp()
names = [os.path.join(tmp, f"part{i}.txt") for i in range(3)]
with ExitStack() as stack:
files = [stack.enter_context(open(n, "w", encoding="utf-8")) for n in names]
for i, f in enumerate(files):
f.write(f"строка {i}\n")
stack.callback(print, "все файлы будут закрыты")
print("открыто файлов:", sum(not f.closed for f in files))
print("закрыто файлов:", sum(f.closed for f in files))
with suppress(FileNotFoundError):
os.remove(os.path.join(tmp, "absent.txt"))
print("удаление несуществующего файла не уронило программу")
for n in names:
os.remove(n)
os.rmdir(tmp)
открыто файлов: 3
все файлы будут закрыты
закрыто файлов: 3
удаление несуществующего файла не уронило программу
Если третий open() упадет, ExitStack все равно закроет два уже открытых файла. Кроме этих двух, в contextlib есть closing (вызывает close() у объекта без протокола with), nullcontext (пустой менеджер для условного кода) и chdir (временная смена каталога, с Python 3.11).
Несколько менеджеров в одном with
Менеджеры можно перечислить через запятую. С Python 3.10 официально разрешены круглые скобки, чтобы разбить длинную строку:
class Step:
def __init__(self, name):
self.name = name
def __enter__(self):
print("enter", self.name)
return self
def __exit__(self, *exc):
print("exit ", self.name)
with (
Step("A") as a,
Step("B") as b,
):
print("тело блока")
enter A
enter B
тело блока
exit B
exit A
Вход идет слева направо, выход — в обратном порядке, как у вложенных with. Если B не смог войти, A все равно будет закрыт. На версиях до 3.10 вместо скобок используйте обратный слеш или ExitStack.
async with: асинхронные менеджеры
В асинхронном коде захват и освобождение ресурса сами бывают корутинами (подключение к базе, HTTP-сессия). Для этого есть async with и методы __aenter__/__aexit__. Писать их можно только внутри async def. Готовый декоратор — asynccontextmanager:
import asyncio
from contextlib import asynccontextmanager
@asynccontextmanager
async def connection(name):
await asyncio.sleep(0.01) # имитация асинхронного подключения
print("подключились к", name)
try:
yield f"conn:{name}"
finally:
await asyncio.sleep(0.01) # асинхронное закрытие
print("отключились от", name)
async def main():
async with connection("db") as conn:
print("работаем через", conn)
asyncio.run(main())
подключились к db
работаем через conn:db
отключились от db
Правило про try/finally вокруг yield здесь то же самое. Обычный with для асинхронного менеджера не подойдет, и наоборот.
Менеджер делает не всегда то, что вы ждете
Что именно происходит в __exit__, решает автор класса, и это не всегда «закрыть». Показательный пример — sqlite3.Connection:
import sqlite3
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE t (x INTEGER)")
with conn: # транзакция: commit или rollback
conn.execute("INSERT INTO t VALUES (1)")
print(conn.execute("SELECT count(*) FROM t").fetchone()) # соединение еще открыто
conn.close()
Программа выведет (1,): with conn зафиксировал транзакцию (при исключении откатил бы), но соединение не закрыл. Закрывать его нужно явно или через contextlib.closing(conn). Перед использованием чужого менеджера смотрите документацию: что он делает на входе и на выходе.
Если не получилось: типовые симптомы
| Симптом | Причина | Что делать |
|---|---|---|
TypeError: '...' object does not support the context manager protocol |
У объекта нет __enter__/__exit__, например генератор без @contextmanager |
Добавить декоратор или методы протокола |
| Исключение из блока молча пропало | __exit__ вернул True или генератор перехватил ошибку |
Возвращать истину только для ожидаемого типа |
| Ресурс не освобождается при ошибке | Нет try/finally вокруг yield |
Обернуть yield в try/finally |
В as оказалось не то |
__enter__ вернул другой объект |
Сверить с документацией менеджера |
Текст ошибки из первой строки на Python 3.14 выглядит так (генератор вместо менеджера):
TypeError: 'generator' object does not support the context manager protocol (missed __exit__ method)
Выводы
- with вызывает
__enter__перед блоком и__exit__после него при любом выходе, включая исключение. - В
asпопадает результат__enter__, а не сам менеджер: у файла это файл, уLock—True. - Истинное значение из
__exit__подавляет исключение; подавлять стоит только ожидаемые типы. @contextmanagerтребуетtry/finallyвокругyield, иначе очистка пропускается при ошибке;ExitStackиsuppressзакрывают типовые задачи без своих классов.- Скобки для нескольких менеджеров официально доступны с Python 3.10; для корутин используется
async with.
Где применяется / связь с практикой
Освойте тему на практике
Контекстные менеджеры встречаются в любом зрелом Python-коде: транзакции и сессии ORM, блокировки в многопоточном коде, временные настройки (decimal.localcontext), фикстуры тестов, асинхронные клиенты. Умение написать свой менеджер и правильно обработать в нем исключения отличает аккуратный production-код от учебного. Протокол with, генераторы, асинхронность и другие механизмы языка системно разбираются на курсе «Python-разработчик. Продвинутый уровень». Отдельные темы можно посмотреть на бесплатных открытых уроках Otus.
FAQ
Можно ли использовать один и тот же менеджер в нескольких with подряд?
Зависит от менеджера. Lock можно захватывать повторно в разных блоках, закрытый файл — нет, а объект из @contextmanager одноразовый: повторный вход в тот же объект завершится ошибкой (на Python 3.14 это AttributeError). Для нового блока вызывайте функцию заново.
Чем with лучше, чем try/finally?
По гарантиям они эквивалентны, with — это обертка над той же схемой. Выигрыш в том, что логика захвата и освобождения живет в одном месте (менеджере) и не дублируется в каждом вызывающем коде.
Появилось ли что-то новое в with в последних версиях Python?
Сам протокол со времен PEP 343 не менялся. Из заметного: скобки для нескольких менеджеров (3.10), contextlib.chdir (3.11) и более понятный текст ошибки при отсутствии протокола (3.11+).



