Imagine the situation: you open a file, work with it, and then forget to close it. Or you open a connection to a database, and suddenly an exception occurs that interrupts the execution of the code before you have time to free up resources. Sound familiar? Context managers in Python solve this problem elegantly and reliably.
Why do we need contextual managers?
Context managers provide automatic resource management. They ensure that a certain code will be executed before and after working with the resource, regardless of whether an error occurred or not.
A classic example without a context manager:
file = open('data.txt', 'r')
try:
content = file.read()
# Working with content
finally:
file.close() # Don't forget to close!With the context manager, everything is much simpler:
with open('data.txt', 'r') as file:
content = file.read()
# The file will automatically close after exiting the block
How does the with construction work?
When Python meets the with operator, the following happens:
The __enter__() method of the object is called
The return value __enter__() is assigned to the variable after as (if specified)
The code is executed inside the with block
After the block is completed (or when an exception occurs), the __exit () method is called
This ensures that the resources are always cleaned up, even if an error occurs inside the block.
Creating your own contextual manager
To create your own context manager, you need to implement two special methods: __enter__ and __exit__.
A simple example: database connection manager
class DatabaseConnection:
def __init__(self, db_name):
self.db_name = db_name
self.connection = None
def __enter__(self):
print(f"Opening connection to {self.db_name}")
self.connection = f"Connection to {self.db_name}"
return self.connection
def __exit__(self, exc_type, exc_value, traceback):
print(f"Closing connection to {self.db_name}")
if exc_type is not None:
print(f"An error occurred: {exc_type.__name__}: {exc_value}")
return False # The exception will be passed on
# Use
with DatabaseConnection("users_db") as conn:
print(f"We work with: {conn}")
# Here is your code for working with the databaseConclusion:
Открываем соединение с users_db
Работаем с: Connection to users_db
Закрываем соединение с users_dbMethod __enter ()
The __enter__() method is called when entering the context. It can:
Initialize resources
Establish connections
Capture locks
Return the object for work (which will be available through as)
def __enter__(self):
# Preparation of resources
self.resource = self.acquire_resource()
return self.resource # This value will be assigned to the variable after 'as'Important: the method can return any object, including self. If you do not need a return value, you can simply use with without as.
Method __exit__(exc_type, exc_value, traceback)
The __exit__() method is called when exiting the context and takes three arguments:
exc_type — exception type (or None if there was no exception)
exc_value — exception object
traceback — traceback object for exception
The return value determines whether the exception will be suppressed:
True — the exception is suppressed (not forwarded further)
False or None — the exception is passed on
Example with exception handling
class ErrorHandler:
def __init__(self, suppress_errors=False):
self.suppress_errors = suppress_errors
def __enter__(self):
print("Starting execution")
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is not None:
print(f"Caught exception: {exc_type.__name__}")
if self.suppress_errors:
print("Exception suppressed")
return True # Suppress exception
print("Completing")
return False # Throwing an exception
# The exception will be suppressed
with ErrorHandler(suppress_errors=True):
print("Executing code")
raise ValueError("Something went wrong!")
print("This line will not be executed")
print("The program continues to work")
Practical examples of use
Code execution timer
import time
class Timer:
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, *args):
self.end = time.time()
self.elapsed = self.end - self.start
print(f"Execution time: {self.elapsed:.4f} seconds")
with Timer():
# Measuring execution time
total = sum(range(1000000))Temporary directory change
import os
class ChangeDirectory:
def __init__(self, new_path):
self.new_path = new_path
self.saved_path = None
def __enter__(self):
self.saved_path = os.getcwd()
os.chdir(self.new_path)
return self
def __exit__(self, *args):
os.chdir(self.saved_path)
with ChangeDirectory('/tmp'):
print(f"Current directory: {os.getcwd()}")
# Working in /tmp
print(f"Returned to: {os.getcwd()}")Transaction management
class Transaction:
def __init__(self, connection):
self.connection = connection
def __enter__(self):
self.connection.begin()
return self.connection
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is None:
self.connection.commit()
print("Transaction confirmed")
else:
self.connection.rollback()
print("Transaction rolled back")
return False
# Use
# with Transaction(db_connection) as conn:
# conn.execute("INSERT INTO users VALUES (...)")
# # If an error occurs, the changes will be rolled backContextlib module — context managers without classes
Python provides the contextlib module, which makes it easier to create context managers using the @contextmanager decorator:
from contextlib import contextmanager
@contextmanager
def managed_file(filename, mode):
print(f"Opening file {filename}")
file = open(filename, mode)
try:
yield file # Everything before yield is __enter__
finally:
print(f"Closing file {filename}")
file.close() # Everything after yield is __exit__
with managed_file('test.txt', 'w') as f:
f.write('Hello, world!')The yield keyword separates the logic: the code before it is executed when entering the context, the code after it is executed when exiting.
Suppressing exceptions with contextlib.suppress
from contextlib import suppress
# Ignore FileNotFoundError
with suppress(FileNotFoundError):
os.remove('non-existent_file.txt')
print("The program continues to work")Multiple Context Managers
Python allows you to use multiple context managers in one with statement:
with open('input.txt', 'r') as infile, open('output.txt', 'w') as outfile:
content = infile.read()
outfile.write(content.upper())This is equivalent to nested blocks:
with open('input.txt', 'r') as infile:
with open('output.txt', 'w') as outfile:
content = infile.read()
outfile.write(content.upper())When to use contextual managers?
Context managers are ideal for situations where you need a paired "setup/clean" operation:
File management
Managing connections to the database
Locks in multithreaded applications
Temporary change in the system state
Transaction management
Measurement of execution time
Logging in and out of code blocks
Tips and best practices
Always free resources in __exit__(). Even if an exception occurs, the __exit__() method will be called, so it must correctly clear the resources.
Use contextlib for simple cases. If you don't need all the power of the class, the @contextmanager decorator will greatly simplify the code.
Be careful with suppressing exceptions. Return True from __exit__() only when you are sure that the exception has been handled correctly.
Context managers can be reused. The same context manager object can be used multiple times if it correctly reinitializes the state.
Test behavior in case of exceptions. Make sure that your context manager works correctly both in normal mode and when errors occur.
Context managers make your code cleaner, safer, and more reliable. They eliminate the need to remember to manually clean up resources and ensure that everything is done correctly, even if something goes wrong. This is one example of how Python helps developers write better code with less effort.
Appendix Code offers structured courses for beginner developers in Python, JavaScript, HTML, CSS, and other technologies. The training is structured so that you can gradually master the material from simple to complex.
Join our Telegram channel, where you will find community support, ask questions, and get help learning programming. Learning together is easier and more interesting!
