with: синтаксис для кода, который обязан выполниться при выходе из блока, даже если блок упал с исключением. Открыл файл, обязан закрыть. Взял блокировку, обязан отпустить. Контекстный менеджер это гарантирует, а try/finally вручную работает только пока не забудешь его написать. Код проверен на Python 3.14.5.
Зачем with: файл без него остаётся открытым
Без with закрытие файла превращается в отдельную строку, а исключение между открытием и ею эту строку пропускает.
f = open("data.txt", "w")
try:
f.write("привет")
raise ValueError("сеть оборвалась")
except ValueError as exc:
print(f"поймали: {exc}")
print("файл закрыт?", f.closed)
поймали: сеть оборвалась
файл закрыт? False
Исключение поймали, программа не упала, а файловый дескриптор так и висит открытым. В скрипте на пять строк это не страшно, в сервисе, который открывает файлы тысячами, это исчерпание дескрипторов через несколько часов работы.
Лечится try/finally: close() переносится в блок, который выполнится в любом случае, и при успехе, и при исключении.
f = open("data.txt", "w")
try:
f.write("привет")
raise ValueError("сеть оборвалась")
except ValueError as exc:
print(f"поймали: {exc}")
finally:
f.close()
print("try/finally: файл закрыт?", f.closed)
поймали: сеть оборвалась
try/finally: файл закрыт? True
with делает ровно то же самое, короче:
try:
with open("data.txt", "w") as f2:
f2.write("привет")
raise ValueError("сеть оборвалась")
except ValueError as exc:
print(f"поймали: {exc}")
print("with: файл закрыт?", f2.closed)
поймали: сеть оборвалась
with: файл закрыт? True
Три строки вместо шести, и cleanup нельзя забыть: он не в теле функции, а в объекте, который знает, как себя закрыть. За этим стоит протокол из двух методов. __enter__ вызывается на входе в блок, __exit__ на выходе, независимо от того, как блок завершился.
class Traced:
def __enter__(self):
print("1. __enter__")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("3. __exit__", exc_type, exc_val)
return False
with Traced() as t:
print("2. тело блока")
1. __enter__
2. тело блока
3. __exit__ None None
Порядок фиксированный: __enter__ → тело → __exit__. Значение, которое вернул __enter__, попадает в переменную после as, и это необязательно self: годится что угодно, включая ничего.
Свой класс-менеджер: таймер блока кода
Класс с __enter__/__exit__ пишется под что угодно, что нужно засечь до и после. Таймер — типовой пример: старт в __enter__, разница в __exit__.
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.elapsed = time.perf_counter() - self.start
print(f"блок занял {self.elapsed:.3f} c")
return False
with Timer() as t:
total = sum(range(20_000_000))
print("сумма:", total)
блок занял 0.161 c
сумма: 199999990000000
self.elapsed остаётся доступным и после блока: __exit__ кладёт его в объект, а не теряет вместе со стеком. Замер остаётся доступен и после with, не только в выводе.
exit и исключения: три параметра и ловушка return True
Если тело блока упало, __exit__ получает подробности исключения тремя аргументами: тип, само исключение, объект трассировки. Это не опциональные параметры для галочки, Python вызывает __exit__ именно так при любом исходе. При успехе все три просто равны None.
class LogsAndPasses:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("exc_type:", exc_type)
print("exc_val:", exc_val)
print("exc_tb:", type(exc_tb).__name__ if exc_tb else None)
return False
try:
with LogsAndPasses():
1 / 0
except ZeroDivisionError as exc:
print("наружу вылетело:", exc)
exc_type: <class 'ZeroDivisionError'>
exc_val: division by zero
exc_tb: traceback
наружу вылетело: division by zero
__exit__ вернул False, и исключение полетело дальше как обычно. Возвращаемое значение здесь решает, подавлять исключение или нет: True глушит его насмерть, with завершается тихо, будто ошибки не было.
class SwallowsEverything:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
return True
with SwallowsEverything():
raise KeyError("важная ошибка")
print("код после блока выполнился, будто ничего не было")
код после блока выполнился, будто ничего не было
KeyError исчез бесследно, вывода про саму ошибку нет вообще. Вот и ловушка: return True в конце __exit__: частая опечатка, когда пишут «верну True, если всё ок» и забывают, что в контексте __exit__ это значит не «всё ок», а «глотай любую ошибку из блока». Ловится не сразу: код продолжает работать, просто результат внутри блока молча не случился. Если менеджеру нужно подавлять конкретное исключение, проверяют exc_type явно, через return exc_type is ValueError, а не голое True.
contextlib.contextmanager: тот же таймер через yield
Писать класс с двумя методами ради одного таймера избыточно. @contextlib.contextmanager строит менеджер из функции-генератора: код до yield работает как __enter__, код после yield — как __exit__. Механика та же, что у генераторов вообще, про заморозку кадра и yield подробно разобрано в статье про генераторы.
import contextlib
import time
@contextlib.contextmanager
def timer():
start = time.perf_counter()
try:
yield
finally:
print(f"блок занял {time.perf_counter() - start:.3f} c")
try:
with timer():
sum(range(10_000_000))
raise ValueError("что-то упало внутри")
except ValueError as exc:
print("поймали снаружи:", exc)
блок занял 0.080 c
поймали снаружи: что-то упало внутри
try/finally вокруг yield здесь не стилистика, а обязательное условие. Исключение из тела with contextlib поднимает прямо в точке yield. Если вокруг неё нет finally, код после yield при падении блока просто не выполнится.
@contextlib.contextmanager
def timer_broken():
start = time.perf_counter()
yield
print(f"блок занял {time.perf_counter() - start:.3f} c") # без finally
try:
with timer_broken():
sum(range(10_000_000))
raise ValueError("что-то упало внутри")
except ValueError as exc:
print("поймали снаружи:", exc, "— а времени в логе нет")
поймали снаружи: что-то упало внутри — а времени в логе нет
Строка с замером времени не напечаталась. При успешном завершении блока разницы не видно, оба варианта отработают одинаково. Расхождение проявляется только на исключении, и именно там cleanup чаще всего и нужен.
Готовые менеджеры: closing, suppress, ExitStack
Три вещи из contextlib, которые закрывают конкретные задачи без своего класса.
closing превращает в менеджер объект, у которого есть close(), но нет __enter__/__exit__: так устроены некоторые сетевые клиенты и старые API.
import contextlib
import os
import tempfile
class Connection:
def __init__(self):
self.open = True
print("соединение открыто")
def close(self):
self.open = False
print("соединение закрыто")
conn = Connection()
with contextlib.closing(conn):
print("работаем, open =", conn.open)
print("после блока, open =", conn.open)
соединение открыто
работаем, open = True
соединение закрыто
после блока, open = False
suppress заменяет except Exception: pass, но с адресом. Пустой except глотает вообще всё, включая KeyboardInterrupt и опечатку в имени переменной внутри блока. suppress(FileNotFoundError) называет конкретное исключение и пропускает остальные наружу, и в этом вся разница, а не в форме записи.
with contextlib.suppress(FileNotFoundError):
os.remove("no-such-file.txt")
print("файла не было, но программа не упала")
try:
with contextlib.suppress(FileNotFoundError):
raise PermissionError("нет прав")
except PermissionError as exc:
print("suppress пропустил чужую ошибку наружу:", exc)
файла не было, но программа не упала
suppress пропустил чужую ошибку наружу: нет прав
ExitStack нужен, когда число ресурсов заранее неизвестно: не два файла, а список из N. Внутри цикла with не завести, а ExitStack.enter_context регистрирует любое число менеджеров и закроет их все на выходе, в обратном порядке.
with contextlib.ExitStack() as stack:
files = [
stack.enter_context(tempfile.NamedTemporaryFile(mode="w", delete=True))
for _ in range(4)
]
for i, f in enumerate(files):
f.write(f"файл {i}")
f.flush()
print("открыто файлов:", len(files))
print("все существуют:", all(os.path.exists(f.name) for f in files))
print("после блока все закрыты:", all(f.closed for f in files))
открыто файлов: 4
все существуют: True
после блока все закрыты: True
Упади исключение на третьем файле из четырёх, ExitStack всё равно закроет уже открытые. Ручной цикл с try/finally на каждый файл писать не нужно.
Где менеджеры уже вокруг тебя
open() — контекстный менеджер, с которого обычно и знакомятся с with. Второй пример: threading.Lock. with lock берёт блокировку на входе в блок и гарантированно отпускает на выходе, даже если тело блока упадёт.
import threading
lock = threading.Lock()
balance = 0
def deposit():
global balance
for _ in range(100_000):
with lock:
balance += 1
threads = [threading.Thread(target=deposit) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print("итоговый баланс:", balance, "ожидалось:", 4 * 100_000)
итоговый баланс: 400000 ожидалось: 400000
Без with lock пришлось бы писать lock.acquire() и lock.release() вручную. При исключении внутри критической секции release() не вызвался бы, поток остался бы держать блокировку навсегда, а остальные три зависли бы на acquire(). Подробнее про потоки и гонки за общими данными читай в статье про threading.
async with — тот же протокол, но асинхронный
У асинхронного кода свой вариант протокола: __aenter__ и __aexit__, обе корутины, обе ждутся через await внутри async with. Нужен он там, где вход и выход в ресурс сами требуют ожидания: открыть соединение с базой асинхронным драйвером, закрыть сессию HTTP-клиента. Пример здесь избыточен, механика async/await и то, почему синхронный блокирующий вызов внутри корутины останавливает весь цикл, а await цикл, наоборот, освобождает, разобраны в статье про asyncio.
Частые вопросы
Обязательно ли писать exit, если исключения не ожидаются
Да. __exit__ — единственное место, куда гарантированно попадает управление на выходе из блока, при ошибке и без неё. Без него менеджер выполнит __enter__ и не закроет ресурс при обычном успешном выполнении блока тоже.
Чем with отличается от декоратора
with оборачивает произвольный блок кода, декоратор — вызов функции целиком. Когда нужны оба варианта на одном объекте, contextlib.ContextDecorator даёт класс, который работает и так, и так, подробности в статье про декораторы.
Что вернуть из exit, чтобы просто залогировать ошибку и пробросить её дальше
False или None — оба варианта не подавляют исключение. return False в конце __exit__ эквивалентен отсутствию return: без явного return функция и так вернёт None, а None в булевом контексте это ложь.