Zephyrnet Logo

How To Create Custom Context Managers in Python – KDnuggets

Date:

custom-context-manager
Image by Author
 

Context managers in Python let you work more efficiently with resources—facilitating setup and teardown of resources even if there are errors when working with the resources. In the tutorial on writing efficient Python code, I covered what context managers are and why they’re helpful. And in 3 Interesting Uses of Python’s Context Managers, I went over the use of context managers in managing subprocesses, database connections, and more.

In this tutorial, you’ll learn how to create your own custom context managers. We’ll review how context manages work and then look at the different ways you can write your own. Let’s get started.

What Are Context Managers in Python?

Context managers in Python are objects that enable the management of resources such as file operations, database connections, or network sockets within a controlled block of code. They ensure that resources are properly initialized before the block of code executes and automatically cleaned up afterward, regardless of whether the code block completes normally or raises an exception.

In general, context managers in Python have the following two special methods: __enter__() and __exit__(). These methods define the behavior of the context manager when entering and exiting a context.

How Do Context Managers Work?

When working with resources in Python, you have to consider setting up the resource, anticipate errors, implement exception handling, and finally free up the resource. To do this, you’ll probably use a try-except-finally block like so:

try: 
    # Setting up the resource
    # Working with the resource
except ErrorType:
    # Handle exceptions
finally:
    # Free up the resource

Essentially, we try provisioning and working with the resource, except for any errors that may arise during the process, and finally free up the resource. The finally block is always executed regardless of whether the operation succeeds or not. But with context managers and the with statement, you can have reusable try-except-finally blocks.

Now let’s go over how context managers work.

Enter Phase (__enter__() method):
When a with statement is encountered, the __enter__() method of the context manager is invoked. This method is responsible for initializing and setting up the resource such as opening a file, establishing a database connection, and the like. The value returned by __enter__() (if any) is made available to the context block after the `as` keyword.

Execute the Block of Code:
Once the resource is set up (after __enter__() is executed), the code block associated with the with statement is executed. This is the operation you want to perform on the resource.

Exit Phase (__exit__() method):
After the code block completes execution—either normally or due to an exception—the __exit__() method of the context manager is called. The __exit__() method handles cleanup tasks, such as closing the resources. If an exception occurs within the code block, information about the exception (type, value, traceback) is passed to __exit__() for error handling.

To sum up:

  • Context managers provide a way to manage resources efficiently by ensuring that resources are properly initialized and cleaned up.
  • We use the with statement to define a context where resources are managed.
  • The __enter__() method initializes the resource, and the __exit__() method cleans up the resource after the context block completes.

Now that we know how context managers work, let’s proceed to write a custom context manager for handling database connections.

Creating Custom Context Managers in Python

You can write your own context managers in Python using one of the following two methods:

  1. Writing a class with __enter__() and __exit__() methods.
  2. Using the contextlib module which provides the contextmanager decorator to write a context manager using generator functions.

1. Writing a Class with __enter__() and __exit__() Methods

You can define a class that implements the two special methods: __enter__() and __exit__() that control resource setup and teardown respectively. Here we write a ConnectionManager class that establishes a connection to an SQLite database and closes the database connection:

import sqlite3
from typing import Optional

# Writing a context manager class
class ConnectionManager:
    def __init__(self, db_name: str):
        self.db_name = db_name
        self.conn: Optional[sqlite3.Connection] = None

    def __enter__(self):
        self.conn = sqlite3.connect(self.db_name)
        return self.conn

    def __exit__(self, exc_type, exc_value, traceback):
        if self.conn:
        self.conn.close()

Let’s break down how the ConnectionManager works:

  • The __enter__() method is called when the execution enters the context of the with statement. It is responsible for setting up the context, in this case, connecting to a database. It returns the resource that needs to be managed: the database connection. Note that we’ve used the Optional type from the typing module for the connection object conn. We use Optional when the value can be one of two types: here a valid connection object or None.
  • The __exit__() method: It’s called when the execution leaves the context of the with statement. It handles the cleanup action of closing the connection. The parameters exc_type, exc_value, and traceback are for handling exceptions within the `with` block. These can be used to determine whether the context was exited due to an exception.

Now let’s use the ConnectionManager in the with statement. We do the following:

  • Try to connect to the database
  • Create a cursor to run queries
  • Create a table and insert records
  • Query the database table and retrieve the results of the query
db_name = "library.db"

# Using ConnectionManager context manager directly
with ConnectionManager(db_name) as conn:
	cursor = conn.cursor()

	# Create a books table if it doesn't exist
	cursor.execute("""
    	CREATE TABLE IF NOT EXISTS books (
        	id INTEGER PRIMARY KEY,
        	title TEXT,
        	author TEXT,
        	publication_year INTEGER
    	)
	""")

	# Insert sample book records
	books_data = [
    	("The Great Gatsby", "F. Scott Fitzgerald", 1925),
    	("To Kill a Mockingbird", "Harper Lee", 1960),
    	("1984", "George Orwell", 1949),
    	("Pride and Prejudice", "Jane Austen", 1813)
	]
	cursor.executemany("INSERT INTO books (title, author, publication_year) VALUES (?, ?, ?)", books_data)
	conn.commit()

	# Retrieve and print all book records
	cursor.execute("SELECT * FROM books")
	records = cursor.fetchall()
	print("Library Catalog:")
	for record in records:
    	    book_id, title, author, publication_year = record
    	    print(f"Book ID: {book_id}, Title: {title}, Author: {author}, Year: {publication_year}")
            cursor.close()

Running the above code should give you the following output:

Output >>>

Library Catalog:
Book ID: 1, Title: The Great Gatsby, Author: F. Scott Fitzgerald, Year: 1925
Book ID: 2, Title: To Kill a Mockingbird, Author: Harper Lee, Year: 1960
Book ID: 3, Title: 1984, Author: George Orwell, Year: 1949
Book ID: 4, Title: Pride and Prejudice, Author: Jane Austen, Year: 1813

2. Using the @contextmanager Decorator From contextlib

The contextlib module provides the @contextmanager decorator which can be used to define a generator function as a context manager. Here’s how we do it for the database connection example:

# Writing a generator function with the `@contextmanager` decorator
import sqlite3
from contextlib import contextmanager

@contextmanager
def database_connection(db_name: str):
    conn = sqlite3.connect(db_name)
    try:
        yield conn  # Provide the connection to the 'with' block
    finally:
        conn.close()  # Close the connection upon exiting the 'with' block

Here’s how the database_connection function works:

  • The database_connection function first establishes a connection which the yield statement then provisions the connection to the block of code in the with statement block. Note that because yield itself is not immune to exceptions, we wrap it in a try block.
  • The finally block ensures that the connection is always closed, whether an exception was raised or not, ensuring there are no resource leaks.

Like we did previously, let’s use this in a with statement:

db_name = "library.db"

# Using database_connection context manager directly
with database_connection(db_name) as conn:
	cursor = conn.cursor()

	# Insert a set of book records
	more_books_data = [
    	("The Catcher in the Rye", "J.D. Salinger", 1951),
    	("To the Lighthouse", "Virginia Woolf", 1927),
    	("Dune", "Frank Herbert", 1965),
    	("Slaughterhouse-Five", "Kurt Vonnegut", 1969)
	]
	cursor.executemany("INSERT INTO books (title, author, publication_year) VALUES (?, ?, ?)", more_books_data)
	conn.commit()

	# Retrieve and print all book records
	cursor.execute("SELECT * FROM books")
	records = cursor.fetchall()
	print("Updated Library Catalog:")
	for record in records:
    	    book_id, title, author, publication_year = record
    	    print(f"Book ID: {book_id}, Title: {title}, Author: {author}, Year: {publication_year}")
        cursor.close()

We connect to the database, insert some more records, query the db, and fetch the results of the query. Here’s the output:

Output >>>

Updated Library Catalog:
Book ID: 1, Title: The Great Gatsby, Author: F. Scott Fitzgerald, Year: 1925
Book ID: 2, Title: To Kill a Mockingbird, Author: Harper Lee, Year: 1960
Book ID: 3, Title: 1984, Author: George Orwell, Year: 1949
Book ID: 4, Title: Pride and Prejudice, Author: Jane Austen, Year: 1813
Book ID: 5, Title: The Catcher in the Rye, Author: J.D. Salinger, Year: 1951
Book ID: 6, Title: To the Lighthouse, Author: Virginia Woolf, Year: 1927
Book ID: 7, Title: Dune, Author: Frank Herbert, Year: 1965
Book ID: 8, Title: Slaughterhouse-Five, Author: Kurt Vonnegut, Year: 1969

Note that we open and close the cursor object. So you can also use the cursor object in a with statement. I suggest trying that as a quick exercise!

Wrapping Up

And that’s a wrap. I hope you learned how to create your own custom context managers. We looked at two approaches: using a class with __enter__() and __exit()__ methods and using a generator function decorated with the @contextmanager decorator.

It’s quite easy to see that you get the following advantages when using a context manager:

  • Setup and teardown of resources are automatically managed, minimizing resource leaks.
  • The code is cleaner and easier to maintain.
  • Cleaner exception handling when working with resources.

As always, you can find the code on GitHub. Keep coding!

 
 

Bala Priya C is a developer and technical writer from India. She likes working at the intersection of math, programming, data science, and content creation. Her areas of interest and expertise include DevOps, data science, and natural language processing. She enjoys reading, writing, coding, and coffee! Currently, she’s working on learning and sharing her knowledge with the developer community by authoring tutorials, how-to guides, opinion pieces, and more. Bala also creates engaging resource overviews and coding tutorials.

spot_img

Latest Intelligence

spot_img