Context Managers in Python (with statement, __enter__, __exit__)

05 Apr 2026, Updated: 14 Jul 2026 4 min read
0
Python provides context managers, a clean and robust way to manage resources automatically.

They are most commonly used with the with statement, which ensures that resource initialization and cleanup are handled correctly, even if an exception occurs.

Without Context Managers

Consider how resource management is typically handled without context managers:
file = open("example.txt", "r")
try:
    content = file.read()
finally:
    file.close()
In this example, we manually ensure that the file is closed using a try-finally block.

Although this approach works, it is verbose and can become error-prone when repeated throughout a large codebase. Forgetting to release resources can lead to resource leaks and other unexpected issues.

The with Statement

Python simplifies this pattern using the with statement, which automatically handles resource management:
with open("example.txt", "r") as file:
    content = file.read()
Here, the file is automatically opened at the beginning of the block and closed when the block exits, regardless of whether an exception occurs.

This is possible because the file object implements the context manager protocol.

What is a Context Manager?

A context manager is any object that defines two special methods:
- __enter__()
- __exit__()

These methods define what happens when the execution enters and exits the context.

How __enter__ and __exit__ Work

When a with statement is executed:
1. The __enter__() method is called.
2. The value returned by __enter__() is assigned to the variable after as.
3. The block inside the with statement is executed.
4. After the block completes (even if an exception occurs), the __exit__() method is called.

Creating a Custom Context Manager (Class-Based)

You can create your own context manager by implementing these two methods.
class MyContextManager:
    def __enter__(self):
        print("Entering the context")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("Exiting the context")
        if exc_type:
            print(f"An exception occurred: {exc_value}")
        return False  # Propagate exception if any

with MyContextManager() as cm:
    print("Inside the block")
Output:
Entering the context
Inside the block
Exiting the context
If an exception occurs inside the block, it is passed to the __exit__ method. The parameters mean:
- exc_type: Type of exception
- exc_value: Exception instance
- traceback: Traceback object

Returning True from __exit__ suppresses the exception, while False allows it to propagate.

Example: File Handling Context Manager

Let's build a custom file handler using a context manager:
class FileManager:
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode
        self.file = None

    def __enter__(self):
        self.file = open(self.filename, self.mode)
        return self.file

    def __exit__(self, exc_type, exc_value, traceback):
        if self.file:
            self.file.close()
        print("File closed")

with FileManager("example.txt", "w") as f:
    f.write("Hello, Context Manager!")
This ensures that the file is always closed properly.

Using contextlib

Writing class-based context managers can sometimes be verbose. Python provides a simpler way using the contextlib module and the @contextmanager decorator.
from contextlib import contextmanager

@contextmanager
def my_context():
    print("Entering")
    yield
    print("Exiting")

with my_context():
    print("Inside block")
Here, everything before yield acts like __enter__(), and everything after yield acts like __exit__().

Handling Exceptions in contextlib

You can also handle exceptions inside a context manager created with contextlib:
from contextlib import contextmanager

@contextmanager
def safe_context():
    try:
        yield
    except Exception as e:
        print(f"Handled exception: {e}")

with safe_context():
    print(10 / 0)
Output:
Handled exception: division by zero
This allows centralized error handling.

Use Cases

The most common use case is file handling, but they are also used for managing database connections, acquiring and releasing locks in multithreading, and handling network resources.

For example, database connections can be safely managed:
# Pseudo example
with database.connect() as conn:
    conn.execute("SELECT * FROM users")
Similarly, context managers are used in threading:
from threading import Lock

lock = Lock()

with lock:
    print("Critical section")
This ensures the lock is always released properly.

Nested Context Managers

You can use multiple context managers in a single with statement:
with open("file1.txt") as f1, open("file2.txt") as f2:
    data1 = f1.read()
    data2 = f2.read()
This makes code cleaner and avoids deeply nested structures.

Context managers provide several key benefits. They ensure automatic resource management, reduce boilerplate code, and improve readability.

They also guarantee that cleanup code runs even in the presence of exceptions, making programs more robust and reliable.

By leveraging the __enter__ and __exit__ methods, they encapsulate setup and cleanup logic in a clean and reusable way.
Nagesh Chauhan

Nagesh Chauhan

Principal Software Engineer • Java • Python • Distributed Systems • AI/ML

Principal Software Engineer with 14+ years of experience designing and delivering large-scale distributed systems, cloud-native applications, and AI-powered platforms.

Passionate about solving complex engineering problems using strong data structures and algorithms, along with expertise in Java, Spring Boot, Python, System Design, Microservices, Cloud, Kafka, Elasticsearch, and Generative AI.

Share this Article

💬 Comments

Join the Discussion