with, автоматически вызывая методы __enter__() при входе и __exit__() при выходе из блока, даже если возникло исключение.  Это делает код чище и безопаснее, предотвращая утечки ресурсов.
Контекстный менеджер в Python - это конструкция, которая позволяет автоматически выполнять определенные действия до и после выполнения блока кода. Он гарантирует, что ресурсы, используемые в блоке кода, будут корректно освобождены, даже если в процессе выполнения произойдет исключение.
Основная задача: Упростить управление ресурсами (например, файлами, сетевыми соединениями, блокировками) и обеспечить их надежное освобождение после использования. Это особенно важно для предотвращения утечек ресурсов и обеспечения стабильности программы.
Как это работает: Контекстный менеджер определяется с помощью двух магических методов:
__enter__(self): Вызывается при входе в блок with.  Он может возвращать объект, который будет присвоен переменной после ключевого слова as. Здесь обычно происходит получение ресурса (например, открытие файла).__exit__(self, exc_type, exc_val, exc_tb): Вызывается при выходе из блока with, независимо от того, было ли исключение или нет.  Его задача - освободить ресурс (например, закрыть файл). Принимает три аргумента: exc_type, exc_val и exc_tb.  Если исключение не произошло, все они равны None. Если произошло, то exc_type - тип исключения, exc_val - значение исключения, exc_tb - traceback.  Если __exit__ возвращает True, то исключение подавляется (то есть не распространяется дальше).Пример использования (с файлами):
with open("my_file.txt", "r") as file:
    data = file.read()
    # Работа с данными из файла
# Файл автоматически закрывается здесь, даже если произойдет ошибка
    В этом примере open() возвращает объект, который является контекстным менеджером. Метод __enter__() открывает файл, а __exit__() закрывает его.  Блок with гарантирует, что файл будет закрыт, даже если внутри блока with возникнет исключение.
Создание собственного контекстного менеджера:
class MyContextManager:
    def __enter__(self):
        print("Entering the context")
        # Здесь можно выделить ресурс
        return self  # Возвращаем сам объект
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Exiting the context")
        # Здесь нужно освободить ресурс
        if exc_type:
            print(f"An exception of type {exc_type} occurred: {exc_val}")
            return True # Подавляем исключение (не рекомендуется, если не уверены, что можете его обработать)
with MyContextManager() as manager:
    print("Inside the context")
    # Здесь можно использовать manager (который является экземпляром MyContextManager)
    # raise ValueError("Something went wrong") # Раскомментируйте для вызова исключения
print("Outside the context")
    Преимущества:
Альтернатива: Блоки try...finally можно использовать для достижения той же цели, но контекстные менеджеры делают код более лаконичным и читаемым.
Примеры стандартных контекстных менеджеров:
open() - для работы с файламиthreading.Lock, threading.RLock, multiprocessing.Lock - для работы с блокировкамиcontextlib.suppress - для подавления определенных исключенийcontextlib.redirect_stdout, contextlib.redirect_stderr - для перенаправления стандартного вывода и вывода ошибокdecimal.localcontext - для временного изменения контекста decimaltimeit.Timer - для измерения времени выполнения кода