Python's with Statement: Manage External Resources Safely

Python's with Statement: Manage External Resources Safely

by Leodanis Pozo Ramos Publication date Aug 13, 2025 Reading time estimate 39m intermediate python

Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding: Context Managers and Using Python's with Statement

Python’s with statement allows you to manage external resources safely by using objects that support the context manager protocol. These objects automatically handle the setup and cleanup phases of common operations.

By using the with statement alongside appropriate context managers, you can focus on your core logic while the context managers prevent resource leaks like unclosed files, unreleased memory, or dangling network connections.

By the end of this tutorial, you’ll understand that:

  • Python’s with statement automates the process of setting up and tearing down computational resources using context managers.
  • Using with reduces code complexity and prevents resource leaks by ensuring proper resource release, even if exceptions occur.
  • A context manager in Python is an object that implements .__enter__() and .__exit__() methods to manage resources safely.

Get ready to learn how Python’s with statement and context managers streamline the setup and teardown phases of resource management so you can write safer, more reliable code.

Take the Quiz: Test your knowledge with our interactive “Context Managers and Python's with Statement” quiz. You’ll receive a score upon completion to help you track your learning progress:


Interactive Quiz

Context Managers and Python's with Statement

Test your knowledge of Python's with statement and context managers to write cleaner code and manage resources safely and efficiently.

Managing External Resources in Python

Properly managing external resources, such as files, locks, and network connections, is a common requirement in programming. Sometimes, a program uses a given resource and doesn’t release the associated memory when it no longer needs the resource. This kind of issue is called a memory leak because the available memory shrinks every time you create a new instance of a resource without releasing the unneeded ones.

Managing resources properly is often a tricky task. It requires setup and teardown phases. The latter phase requires you to perform cleanup actions, like closing a file, releasing a lock, or closing a network connection. If you forget to perform these cleanup actions, then your application keeps the resource occupied. This behavior might compromise valuable system resources, such as memory and network bandwidth.

For example, say that a program that uses databases keeps creating new connections without releasing the old ones or reusing them. In that case, the database back end can stop accepting new connections. This might require an administrator to log in and manually terminate those stale connections to make the database usable again.

Another common issue occurs when developers work with files. Writing text to files is usually a buffered operation. This means that calling .write() on a file won’t immediately result in writing text to the physical file, but to a temporary buffer. Sometimes, when the buffer isn’t full, developers forget to call .close() and part of the data can be lost.

Another possibility is that your application runs into errors or exceptions that cause the control flow to bypass the code responsible for releasing the resource at hand. Here’s an example where you use the built-in open() function to write some text to a file:

Python
file = open("hello.txt", "w")
file.write("Hello, World!")
file.close()

This code doesn’t guarantee the file will be closed if an exception occurs during the call to .write(). In this situation, the code might never call .close(), and your program will leak a file descriptor. Failing to release a file descriptor on some operating systems can prevent other programs from accessing the underlying file.

In Python, you can use a couple of general approaches to deal with resource management. You can wrap your code in:

  1. A tryfinally construct
  2. A with construct

The first approach is quite generic and allows you to provide setup and teardown code to manage any kind of resource. However, it’s a little bit verbose, and you might forget some cleanup actions if you use this construct in several places.

The second approach provides a straightforward way to provide and reuse setup and teardown code. In this case, you’ll have the limitation that the with statement only works with context managers. In the next two sections, you’ll learn how to use both approaches in your code.

The tryfinally Construct

Working with files is probably the most common example of resource management in programming. In Python, you can use a tryfinally construct to handle opening and closing files properly:

Python
file = open("hello.txt", "w")

try:
    file.write("Hello, World!")
finally:
    file.close()

In this example, you open the hello.txt file using open(). To write some text into the file, you wrap the call to .write() in a try statement with a finally clause. This clause guarantees that the file is properly closed by calling .close(), even if an exception occurs during the call to .write() in the try clause. Remember that the finally clause always runs.

When managing external resources in Python, you can use the construct in the previous example to handle setup and teardown logic. The setup logic might include opening the file and writing content to it, while the teardown logic might consist of closing the file to release the acquired resources.

The try block in the example above can potentially raise exceptions, such as AttributeError or NameError. You can handle these exceptions with an except clause, as shown in the following example:

Python
file = open("hello.txt", "w")

try:
    file.write("Hello, World!")
except Exception as e:
    print(f"An error occurred while writing to the file: {e}")
finally:
    file.close()

In this example, you use the Exception class to catch exceptions that can occur while writing to the file. Then, you print an error message to inform the user. The finally clause always runs so that the file is closed correctly.

Even though the tryfinally construct works correctly, Python has a more compact and elegant solution for you: the with statement.

The with Statement

The Python with statement creates a runtime context that allows you to execute a code block under the control of a context manager. PEP 343 added the with statement to make it possible to factor out common use cases of the tryfinally construct.

Compared to the traditional tryfinally construct, the with statement makes your code clearer, safer, and more readable. Many classes and objects in the standard library support the with statement by implementing the context manager protocol (special methods). For example, when you call open(), you get a file object that supports this protocol and, therefore, the with statement.

To write a with statement, you need to use the following general syntax:

Python Syntax
with expression [as obj]:
    <block>

The context manager object results from evaluating the expression after the with keyword. In other words, expression must return an object that implements the context management protocol. This protocol consists of two special methods:

  1. .__enter__() is called by the with statement to enter the runtime context.
  2. .__exit__() is called when the execution leaves the with code block.

The as specifier is optional. If you provide an obj variable with as, then the return value of calling .__enter__() on the context manager object is bound to that variable.

Here’s how the with statement works internally:

  1. Execute expression to obtain a context manager object.
  2. Call .__enter__() on the context manager and bind its return value to obj if provided.
  3. Execute the with code block.
  4. Call .__exit__() on the context manager when the with code block finishes.

The .__enter__() method typically provides the setup logic. The with statement is a compound statement that starts a code block, like other compound statements, such as conditional statements and for loops.

Inside this code block, you can run one or more statements. Typically, you use the with code block to manipulate obj if applicable.

Once the with code block finishes, Python automatically calls .__exit__(). This method typically provides the teardown logic or cleanup code, such as calling .close() on an open file object. That’s why the with statement is so useful. It makes properly acquiring and releasing resources a breeze.

Here’s how to open your hello.txt file for writing using the with statement:

Python
with open("hello.txt", mode="w", encoding="utf-8") as file:
    file.write("Hello, World!")

When you run this with statement, open() returns an io.TextIOBase object. This object supports the context manager protocol, so the with statement calls .__enter__() and assigns its return value to file. Then, you can manipulate the file inside the with code block. When the block ends, Python automatically calls .__exit__(), which closes the file for you, even if an exception occurs in the with code block.

This with construct is shorter than its tryfinally equivalent construct, but it’s also less generic because you can only use the with statement with objects that support the context management protocol. In contrast, tryfinally allows you to perform cleanup actions for any object, even if it doesn’t implement the context management protocol.

The with statement also supports multiple context managers. You can supply any number of context managers separated by commas:

Python Syntax
with A() as a, B() as b:
    <block>

This works like nested with statements, but without nesting. It might be useful when you need to open two files at a time—the first for reading and the second for writing:

Python
with open("hello.txt") as in_file, open("output.txt", "w") as out_file:
    for line in in_file:
        out_file.write(line.upper())

In this example, you read and transform the content of hello.txt line by line. Then, you write each processed line to output.txt within the same code block.

The with statement can make the code that deals with system resources more readable, concise, and safer. It helps avoid resource leaks by making it almost impossible to forget to clean up, close, and release resources after you’re done with them.

Using with allows you to abstract away most of the resource handling logic. Instead of using explicit tryfinally constructs with setup and teardown logic, you can pack this logic into a context manager and handle it using the with statement to avoid repetition.

Using Python’s with Statement

The with statement is useful in several common situations. Several objects in Python’s standard library provide support for the context manager protocol, so you can use them in with statements for multiple tasks that imply setup and teardown logic.

In the following sections, you’ll code some examples that show how to use the with statement with built-in objects, standard-library classes, and third-party libraries.

Working With Files

So far, you’ve used open() to manipulate files in a with construct. Managing files this way is generally recommended because it ensures that opened file descriptors are automatically closed after the flow of execution leaves the with code block.

Again, a common way to open a file in a with statement is through the open() function:

Python
with open("hello.txt", mode="w", encoding="utf-8") as file:
    file.write("Hello, World!")

In this example, the context manager closes the file after leaving the with code block. A common mistake you might encounter is shown below:

Python
>>> file = open("hello.txt", mode="w", encoding="utf-8")

>>> with file:
...     file.write("Hello, World!")
...
13

>>> with file:
...     file.write("Welcome to Real Python!")
...
Traceback (most recent call last):
    ...
ValueError: I/O operation on closed file.

The first with successfully writes "Hello, World!" into hello.txt. Note that .write() returns the number of bytes written into the file—13 bytes in this example. When you try to run a second with, you get a ValueError because the file is already closed.

Another way to use the with statement to open and manage files is by using the .open() method on a Path object:

Python
>>> import pathlib

>>> file_path = pathlib.Path("hello.txt")

>>> with file_path.open("w", encoding="utf-8") as file:
...     file.write("Hello, World!")
...
13

Path is a class that represents concrete paths to physical files on your computer. Calling .open() on a Path object that points to a physical file opens it just like open() would. So, Path.open() works similarly to open(), but the file path is automatically provided by the Path object you call the method on.

The pathlib package provides an elegant, straightforward, and Pythonic way to manipulate file system paths. You should consider using Path.open() in your with statements as a best practice in Python.

Finally, whenever you load an external file, your program should check for possible issues, such as a missing file, writing and reading access, and so on. Here’s a pattern that you should consider using when working with files:

Python
>>> import pathlib
>>> import logging

>>> file_path = pathlib.Path("/hello.txt")

>>> try:
...     with file_path.open(mode="w") as file:
...         file.write("Hello, World!")
... except OSError as error:
...     logging.error("Writing to file %s failed due to: %s", file_path, error)
...
ERROR:root:Writing to file /hello.txt failed due to: [Errno 13]
⮑ Permission denied: '/hello.txt'

In this example, you wrap the with statement in a tryexcept block. If an OSError occurs during the execution of with, then you use the logging module to log the error with a user-friendly and descriptive message.

Traversing Directories

The os module provides a function called scandir() that returns an iterator over os.DirEntry objects corresponding to the entries in a given directory. This function is specially designed to provide optimal performance when you’re traversing a directory structure.

A call to scandir() with a directory path as an argument returns an iterator that supports the context management protocol:

Python
>>> import os

>>> with os.scandir(".") as entries:
...     for entry in entries:
...         print(entry.name, "->", entry.stat().st_size, "bytes")
...
Documents -> 4096 bytes
Videos -> 12288 bytes
Desktop -> 4096 bytes
DevSpace -> 4096 bytes
.profile -> 807 bytes
Templates -> 4096 bytes
Pictures -> 12288 bytes
Public -> 4096 bytes
Downloads -> 4096 bytes

In this example, you write a with statement with os.scandir() as the context manager supplier. Then, you iterate over the entries in the working directory represented by "." and print their names and sizes on the screen. In this case, .__exit__() calls scandir.close() to close the iterator and release the acquired resources.

Note that if you run this example on your machine, you’ll get a different output depending on the content of your target directory.

Performing High-Precision Calculations

The decimal module provides a handy way to tweak the precision to use in a given calculation that involves decimal numbers. The precision defaults to 28 places, but you can change it to meet your specific requirements.

A quick way to perform calculations with a custom precision is using localcontext() from decimal:

Python
>>> from decimal import Decimal, localcontext

>>> with localcontext(prec=42):
...     Decimal("1") / Decimal("42")
...
Decimal('0.0238095238095238095238095238095238095238095')

>>> Decimal("1") / Decimal("42")
Decimal('0.02380952380952380952380952381')

Here, localcontext() returns a context manager that creates a context allowing you to perform calculations using a custom precision. In the with code block, you have a precision of 42 places. When the with code block finishes, the precision is back to its default value of 28 places.

Handling Locks in Multithreaded Programs

The threading.Lock class in the Python standard library also supports the with statement. This class provides a primitive lock to prevent multiple threads from modifying a shared resource at the same time in a multithreaded application.

You can use a Lock object in a with statement to automatically acquire and release a given lock. For example, say you need to protect the balance of a bank account:

Python
import threading

balance_lock = threading.Lock()

# Use the try ... finally pattern
balance_lock.acquire()
try:
    # Update the account balance here ...
finally:
    balance_lock.release()

# Use the with pattern
with balance_lock:
    # Update the account balance here ...

The with statement in the second example automatically acquires and releases a lock when the flow of execution enters and leaves the statement. This way, you can focus on what really matters in your code and forget about those repetitive operations.

In this example, the lock in the with statement creates a protected region known as the critical section, which prevents concurrent access to the account balance.

Testing for Exceptions With pytest

So far, you’ve coded a few examples using context managers that are available in the Python standard library. However, several third-party libraries include objects that also support the context management protocol.

Say you’re testing your code with pytest. Some of your functions and code blocks raise exceptions under certain situations, and you want to test those cases. To do that, you can use pytest.raises(), which allows you to assert that a block of code or a function call raises a specific exception.

The pytest.raises() function returns a context manager, so you can use that object in a with statement:

Python
>>> import pytest

>>> 1 / 0
Traceback (most recent call last):
    ...
ZeroDivisionError: division by zero

>>> with pytest.raises(ZeroDivisionError):
...     1 / 0
...

>>> favorites = {"fruit": "apple", "pet": "dog"}
>>> favorites["car"]
Traceback (most recent call last):
    ...
KeyError: 'car'

>>> with pytest.raises(KeyError):
...     favorites["car"]
...

In the first example, you use pytest.raises() to capture the ZeroDivisionError exception that the expression 1 / 0 raises. The second example uses this function to capture the KeyError that’s raised when you access a key that doesn’t exist in a given dictionary.

If your function or code block doesn’t raise the expected exception, then pytest.raises() raises a failure exception:

Python
>>> with pytest.raises(ZeroDivisionError):
...     4 / 2
...
2.0
Traceback (most recent call last):
  ...
Failed: DID NOT RAISE <class 'ZeroDivisionError'>

Another cool feature of pytest.raises() is that you can specify a target variable to inspect the raised exception. For example, if you want to verify the error message, then you can do something like this:

Python
>>> with pytest.raises(ZeroDivisionError) as exc:
...     1 / 0
...

>>> assert str(exc.value) == "division by zero"

You can use all these pytest.raises() features to capture the exceptions you raise from your functions and code blocks. This is a cool and useful tool that you can incorporate into your current testing strategy.

Summarizing the with Statement’s Advantages

To summarize what you’ve learned so far, here’s a non-exhaustive list of the general benefits of using the with statement:

  • Makes resource management safer than using equivalent tryfinally constructs
  • Encapsulates standard uses of tryfinally constructs in context managers
  • Allows using context managers to pack the code that handles setup and teardown logic
  • Helps prevent resource leaks

Using the with statement consistently can improve the general quality of your code and make it safer by preventing resource leak problems.

Using the async with Statement

The with statement also has an asynchronous version, async with. You can use it to write context managers that work within asynchronous code. It’s quite common to see async with in that kind of code, as many I/O-bound tasks involve setup and teardown phases.

For example, say you need to code an asynchronous function to check whether some websites are online. To do that, you can use aiohttp, and asyncio. Note that aiohttp is a third-party package that you need to install by running python -m pip install aiohttp on your command line.

Here’s a quick script that implements the required functionality:

Python site_checker_v1.py
 1import asyncio
 2import aiohttp
 3
 4async def check(url):
 5    async with aiohttp.ClientSession() as session:
 6        async with session.get(url) as response:
 7            print(f"{url}: status -> {response.status}")
 8            html = await response.text()
 9            print(f"{url}: type -> {html[:17].strip()}")
10
11async def main():
12    await asyncio.gather(
13        check("https://realpython.com"),
14        check("https://pycoders.com"),
15    )
16
17asyncio.run(main())

Here’s a breakdown of what this script does:

  • Line 1 imports asyncio, which allows you to write concurrent code using the async and await keywords.
  • Line 2 imports aiohttp, which provides an asynchronous HTTP client and server for asyncio and Python.
  • Line 4 defines check() as an asynchronous or coroutine function using the async def keywords.

Inside check(), you use two async with statements, one nested in the other:

  • Line 5 defines an outer async with that instantiates aiohttp.ClientSession() to get a session object that supports the context manager protocol.
  • Line 6 defines an inner async with statement that calls .get() on the session using a URL as an argument. This creates a response object that also supports the context manager protocol.
  • Line 7 prints the response status code for the URL at hand.
  • Line 8 runs an awaitable call to .text() on response and stores the result in html.
  • Line 9 prints the site URL and its document type, doctype.
  • Line 11 defines the script’s main() function, which is also a coroutine function.
  • Line 12 calls gather(), which runs awaitable objects concurrently. In this example, gather() runs two instances of check() with a different URL for each.
  • Line 17 runs main() using asyncio.run(), which creates an event loop and closes it at the end of the operation.

If you run this script from your command line, then you’ll get output similar to the following:

Shell
$ python site_checker_v1.py
https://realpython.com: status -> 200
https://pycoders.com: status -> 200
https://pycoders.com: type -> <!doctype html>
https://realpython.com: type -> <!doctype html>

Cool! Your script works—both sites are currently available. You’ve also successfully retrieved the document type from each site’s home page.

The async with statement works like a regular with statement, but it requires an asynchronous context manager. In other words, it needs a context manager that’s able to suspend execution in its enter and exit methods.

Asynchronous context managers implement the special methods .__aenter__() and .__aexit__(), which are the asynchronous counterparts to .__enter__() and .__exit__() in regular context managers.

When you use the async with ctx_mgr construct, Python implicitly calls await ctx_mgr.__aenter__() on entry and await ctx_mgr.__aexit__() on exit. This allows asynchronous context management to work seamlessly.

Creating Custom Context Managers

You’ve already worked with context managers from the standard library and third-party libraries. There’s nothing special or magical about open(), threading.Lock, decimal.localcontext(), and the others. They simply return objects that implement the context management protocol.

You can provide the same functionality by implementing the .__enter__() and .__exit__() special methods in your class-based context managers. You can also create custom function-based context managers using the contextlib.contextmanager decorator from the standard library and an appropriately coded generator function.

In practice, context managers and the with statement aren’t limited to resource management. They allow you to provide and reuse common setup and teardown code. In other words, you can use a context manager to handle any pair of operations that must occur before and after a task or procedure, such as:

  • Open and close
  • Lock and release
  • Change and reset
  • Create and delete
  • Enter and exit
  • Start and stop
  • Install and uninstall

You can provide code to safely manage any of these pairs of operations in a context manager. Then, you can reuse that context manager in with statements throughout your code. This helps prevent errors, reduces repetitive boilerplate code, and makes your APIs safer, cleaner, and more user-friendly.

In the next two sections, you’ll learn the basics of creating class-based and function-based context managers.

Coding Class-Based Context Managers

To create a class-based context manager, you need to add the .__enter__() and .__exit__() special methods to your class. The table below summarizes how these methods work, the arguments they take, and the kind of logic you can include:

Method Description
.__enter__(self) This method handles the setup logic and is called automatically when entering a new context using the with statement. Its return value is bound to the with target variable.
.__exit__(self, exc_type, exc_value, exc_tb) This method handles the teardown logic and is automatically called when the flow of execution leaves the with block. If an exception occurs, then exc_type, exc_value, and exc_tb hold the exception type, value, and traceback information, respectively.

Next, you’ll learn more about writing your own context manager and dive a bit deeper into the implementation of these special methods.

Writing a Demo Class-Based Context Manager

When the with statement executes, Python calls .__enter__() on the context manager object to set up the new runtime context. If you provide a target variable with the as specifier, then the return value of .__enter__() is assigned to that variable. When the flow of execution leaves the context, .__exit__() is called.

Below is a demo class-based context manager that shows how Python calls the .__enter__() and .__exit__() methods in a with construct:

Python
>>> class HelloContextManager:
...     def __enter__(self):
...         print("Entering the context...")
...         return "Hello, World!"
...
...     def __exit__(self, exc_type, exc_value, exc_tb):
...         print("Leaving the context...")
...         print(f"{exc_type  = }")
...         print(f"{exc_value = }")
...         print(f"{exc_tb    = }")
...
>>> with HelloContextManager() as hello:
...     print(hello)
...
Entering the context...
Hello, World!
Leaving the context...
exc_type  = None
exc_value = None
exc_tb    = None

In .__enter__(), you print a message to indicate that the flow of execution is entering a new context. Then, you return the "Hello, World!" string. In .__exit__(), you print a message to signal that the execution flow is leaving the context. You also print the content of the three arguments.

When the with statement runs, Python creates a new instance of HelloContextManager and calls its .__enter__() method. You know this because you get Entering the context... printed on the screen. Next, Python runs the with code block, which prints hello to the screen. Note that hello holds the return value of .__enter__(), which is "Hello, World!" in this example.

When the execution flow exits the with code block, Python calls .__exit__(). You know that because you get Leaving the context... printed on your screen. If no exception occurs in the with code block, then the three arguments to .__exit__() are set to None. You confirm this behavior in the final output lines.

Now, what if an exception occurs during the execution of the with block? To find out, go ahead and run the following with statement:

Python
>>> with HelloContextManager() as hello:
...     print(hello)
...     hello[100]
...
Entering the context...
Hello, World!
Leaving the context...
exc_type  = <class 'IndexError'>
exc_value = IndexError('string index out of range')
exc_tb    = <traceback object at 0x7f0cebcdd080>
Traceback (most recent call last):
  ...
IndexError: string index out of range

In this example, you try to retrieve the value at index 100 in the string "Hello, World!". This action raises an IndexError exception because the string doesn’t have 100 characters. Therefore, the arguments to .__exit__() are set as follows:

  • exc_type: the exception class, IndexError.
  • exc_value: the concrete exception object with a descriptive message.
  • exc_tb: the traceback object, <traceback object at 0x7f0cebcdd080>.

This behavior is especially useful when you want your context manager to handle exceptions internally.

Handling Exceptions Within Context Managers

If the .__exit__() method returns True, then any exception that occurs in the with block is swallowed and the execution continues at the next statement after with. If .__exit__() returns False, then exceptions are propagated out of the context. This is also the default behavior when the method doesn’t return anything explicitly.

You can take advantage of these behaviors to encapsulate exception handling inside the context manager or to propagate the exceptions as needed.

As an example of encapsulating exception handling in a context manager, say you expect IndexError to be a possible exception when you’re working with HelloContextManager. You might want to handle that exception in the context manager so you don’t have to repeat the exception-handling code in every with code block.

In that situation, you can do something like the following:

Python
>>> class HelloContextManager:
...     def __enter__(self):
...         print("Entering the context...")
...         return "Hello, World!"
...
...     def __exit__(self, exc_type, exc_value, exc_tb):
...         print("Leaving the context...")
...         if isinstance(exc_value, IndexError):
...             # Handle IndexError here...
...             print(f"An exception occurred in your with block: {exc_type}")
...             print(f"Exception message: {exc_value}")
...             return True
...

>>> with HelloContextManager() as hello:
...     print(hello)
...     hello[100]
...
Entering the context...
Hello, World!
Leaving the context...
An exception occurred in your with block: <class 'IndexError'>
Exception message: string index out of range

In .__exit__(), you check if exc_value is an instance of IndexError using the built-in isinstance() function. If so, you print a couple of informative messages and finally return with True.

Again, returning a True makes it possible to swallow the exception and continue the normal execution after the with code block.

In this example, if no IndexError occurs, then the method implicitly returns None, and the exception propagates out of the context. This is the desired behavior because it avoids hiding unexpected exceptions. If you want to be more explicit, then you can return False outside the if block.

Your HelloContextManager class is now able to handle IndexError exceptions that occur in the with code block. Since you return True when an IndexError occurs, the execution flow continues in the next line, right after exiting the with code block.

Opening Files for Writing

Now that you know how to implement the context management protocol, you can code a more realistic example. For instance, below is how you can create a context manager that opens files for writing directly:

Python
>>> class WritableFile:
...     def __init__(self, file_path):
...         self.file_path = file_path
...
...     def __enter__(self):
...         self.file_obj = open(self.file_path, mode="w")
...         return self.file_obj
...
...     def __exit__(self, *_):
...         if self.file_obj:
...             self.file_obj.close()
...

WritableFile allows you to open the file for writing using the "w" mode of open(). You do this in the .__enter__() method. In the .__exit__() method, you close the file to release the acquired resources. Note how you use the *args syntax to maintain compatibility with the context management protocol. Since you don’t need these arguments, you use a single underscore to suppress a potential linter warning about unused variables.

Here’s how you can use your context manager:

Python
>>> with WritableFile("hello.txt") as file:
...    file.write("Hello, World!")
...
13

After running this code, you’ll have a hello.txt file containing the "Hello, World!" string. As an exercise, you can write a complementary context manager that opens files for reading, but using pathlib functionalities. Go ahead and give it a try!

Redirecting the Standard Output to a File

A subtle detail to consider when you’re implementing the context manager protocol is that sometimes you don’t have a useful object to return from .__enter__()—and therefore nothing meaningful to assign to the target variable in the with header. In those cases, you can explicitly return None, or just rely on Python’s implicit return value, which is also None.

For example, say you need to temporarily redirect the standard output, sys.stdout, to a given file on your disk. To do this, you can create a context manager like the following:

Python
>>> import sys

>>> class StandardOutputRedirector:
...     def __init__(self, new_output):
...         self.new_output = new_output
...
...     def __enter__(self):
...         self.std_output = sys.stdout
...         sys.stdout = self.new_output
...
...     def __exit__(self, *_):
...         sys.stdout = self.std_output
...

This context manager takes a file object as an argument. In .__enter__(), you reassign the standard output, sys.stdout, to an instance attribute to avoid losing the reference to it. Then, you reassign the standard output to point to the file on your disk. In .__exit__(), you restore the standard output to its original value.

To use StandardOutputRedirector, you can do something like this:

Python
>>> with open("hello.txt", "w") as file:
...     with StandardOutputRedirector(file):
...         print("Hello, World!")
...     print("Back to the standard output...")
...
Back to the standard output...

The outer with statement provides the file object that you’ll use as your new output, hello.txt. The inner with temporarily redirects the standard output to hello.txt, so the first call to print() writes directly to that file instead of printing "Hello, World!" to your screen. When you leave the inner with code block, the standard output is set back to its original value.

StandardOutputRedirector is a quick example of a context manager that doesn’t have a useful value to return from .__enter__(). In this example, you rely on Python’s implicit behavior of returning None.

Measuring Execution Time

Just like any other class, a context manager can encapsulate some internal state. The following example shows how to create a stateful context manager to measure the execution time of a given piece of code:

Python
>>> from time import perf_counter, sleep

>>> class Timer:
...     def __enter__(self):
...         self.start = perf_counter()
...
...     def __exit__(self, *_):
...         end = perf_counter()
...         print(f"Elapsed time: {end - self.start:.4f} seconds")
...

>>> with Timer():
...     # The code to measure goes here...
...     sleep(0.5)
...
Elapsed time: 0.5051 seconds

In the .__enter__() method, you call time.perf_counter() to get the current timestamp at the beginning of a with code block and store it in the .start attribute. This value represents the context manager’s initial state.

Once the with block ends, .__exit__() gets called. The method gets the time at the end of the block and stores the value in a local variable end. Finally, it prints a message to inform you of the elapsed time.

Indenting Text

What if the resource you wanted to manage is the text indentation level in some kind of report generator application? For example, say that you want to write code that generates HTML output like the following:

HTML
<div>
  <p>
    Hello, World!
  </p>
</div>

This could be a great exercise to help you understand how context managers work. So, before you check out the implementation, you might take some time and try to produce this output with the help of a context manager that you can use as shown below:

Python
with Indenter() as indenter:
    indenter.print("<div>")
    with indenter:
        indenter.print("<p>")
        with indenter:
            indenter.print("Hello, World!")
        indenter.print("</p>")
    indenter.print("</div>")

Notice how this code enters and leaves the same context manager multiple times to switch between different indentation levels.

Ready? Here’s how you can implement this functionality using a class-based context manager:

Python
class Indenter:
    def __init__(self, width=2):
        self.indentation = " " * width
        self.level = -1

    def __enter__(self):
        self.level += 1
        return self

    def __exit__(self, *_):
        self.level -= 1

    def print(self, text):
        print(self.indentation * self.level + text)

Here, .__enter__() increments .level by 1 every time the flow of execution enters the context. The method also returns the current instance, self. In .__exit__(), you decrease .level, so the printed text steps back one indentation level each time you exit the context.

The key point here is that returning self from .__enter__() allows you to reuse the same context manager across several nested with statements. This changes the text indentation level every time you enter and exit a given context.

A good exercise for you at this point would be to write a function-based version of this context manager. Function-based context managers is what you’ll be learning about next.

Creating Function-Based Context Managers

Python’s contextlib.contextmanager decorator provides an alternative and convenient way to implement the context management protocol. If you decorate an appropriately coded generator function with @contextmanager, then you get a function-based context manager that automatically provides both .__enter__() and .__exit__().

The pattern to create a context manager using @contextmanager along with a generator function goes like this:

Python
>>> from contextlib import contextmanager

>>> @contextmanager
... def hello_context_manager():
...     print("Entering the context...")
...     yield "Hello, World!"
...     print("Leaving the context...")
...

>>> with hello_context_manager() as hello:
...     print(hello)
...
Entering the context...
Hello, World!
Leaving the context...

In this example, you can identify two visible sections in hello_context_manager(). Before the yield statement, you have the setup section. There, you can place the code that acquires the needed resources. Everything before the yield runs when the flow of execution enters the context.

After the yield statement, you have the teardown section where you can release resources and do the cleanup. The code after yield runs at the end of the with block. The yield statement itself optionally provides the object that will be assigned to the with target variable, if any.

This implementation and the one that uses the context management protocol are equivalent. Depending on which one you find suitable for your current problem, you might choose one over the other. A downside of the function-based implementation is that it requires an understanding of advanced Python topics, such as decorators and generators.

The @contextmanager decorator reduces the boilerplate required to create a context manager. Instead of writing a whole class with .__enter__() and .__exit__() methods, you just need to implement a generator function with a single yield that produces whatever you want .__enter__() to return.

For a more realistic example, you can use the @contextmanager to reimplement your WritableFile context manager. Here’s what rewriting it with this technique will look like:

Python
>>> from contextlib import contextmanager

>>> @contextmanager
... def writable_file(file_path):
...     try:
...         file = open(file_path, mode="w")
...         yield file
...     finally:
...         file.close()
...

>>> with writable_file("hello.txt") as file:
...     file.write("Hello, World!")
...
13

In this case, writable_file() is a generator function that opens a file for writing. Then, it temporarily suspends its execution and yields the file so that with can bind it to its target variable. When the execution flow leaves the with code block, the function continues to execute and closes the file correctly.

Creating an Asynchronous Context Manager

To create an asynchronous context manager, you need to define the .__aenter__() and .__aexit__() methods. The script below is a reimplementation of the example about checking whether a website is online:

Python site_checker_v2.py
import asyncio
import aiohttp

class AsyncSession:
    def __init__(self, url):
        self._url = url

    async def __aenter__(self):
        self.session = aiohttp.ClientSession()
        response = await self.session.get(self._url)
        return response

    async def __aexit__(self, exc_type, exc_value, exc_tb):
        await self.session.close()

async def check(url):
    async with AsyncSession(url) as response:
        print(f"{url}: status -> {response.status}")
        html = await response.text()
        print(f"{url}: type -> {html[:17].strip()}")

async def main():
    await asyncio.gather(
        check("https://realpython.com"),
        check("https://pycoders.com"),
    )

asyncio.run(main())

This script works like the previous implementation. The main difference is that, in this example, you extract the logic of the original outer async with statement and encapsulate it in AsyncSession.

In .__aenter__(), you create an aiohttp.ClientSession(), await the .get() response, and finally return the response itself. In .__aexit__(), you close the session, which corresponds to the teardown logic in this specific case. Note that .__aenter__() and .__aexit__() must return awaitable objects, so you need to define them using the async def keywords.

If you run the script from your command line, then you get an output similar to this:

Shell
$ python site_checker_v2.py
https://realpython.com: status -> 200
https://pycoders.com: status -> 200
https://realpython.com: type -> <!doctype html>
https://pycoders.com: type -> <!doctype html>

Great! Your script works just like its first version. It sends GET requests to both sites concurrently and processes the corresponding responses.

Finally, a common practice when you’re writing asynchronous context managers is to implement the four special methods:

  1. .__aenter__()
  2. .__aexit__()
  3. .__enter__()
  4. .__exit__()

Doing so makes your context manager usable with both with and async with statements.

Conclusion

You’ve learned about Python’s with statement, which helps you manage external resources safely. You’ve also learned how context managers can automatically handle the setup and teardown phases of working with external resources, and how these tools can prevent resource leaks and improve code safety.

Additionally, you learned how to create custom context managers using both class-based and function-based approaches, and even how to implement asynchronous context managers for concurrent programming.

The with statement is an essential tool for Python developers. It facilitates resource management and ensures that resources are properly released, even in the presence of exceptions.

In this tutorial, you’ve learned how to:

  • Use Python’s with statement for resource management
  • Understand the context manager protocol
  • Create custom class-based and function-based context managers
  • Work with asynchronous context managers for concurrent tasks

With these skills, you can write more reliable and safer Python code by managing resources correctly and avoiding potential leaks. This knowledge strengthens your ability to build robust, maintainable applications.

Frequently Asked Questions

Now that you have some experience with context managers and the with statement in Python, you can use the questions and answers below to check your understanding and recap what you’ve learned.

These FAQs are related to the most important concepts you’ve covered in this tutorial. Click the Show/Hide toggle beside each question to reveal the answer.

You use Python’s with statement to manage resource setup and teardown automatically, ensuring resources are properly released after use, even if exceptions occur.

The with statement reduces code complexity, enhances readability, and—more importantly—prevents resource leaks by ensuring resources are released correctly after use.

A context manager is a Python object that implements the .__enter__() and .__exit__() special methods to safely manage resources by automating setup and teardown steps.

Take the Quiz: Test your knowledge with our interactive “Context Managers and Python's with Statement” quiz. You’ll receive a score upon completion to help you track your learning progress:


Interactive Quiz

Context Managers and Python's with Statement

Test your knowledge of Python's with statement and context managers to write cleaner code and manage resources safely and efficiently.

Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding: Context Managers and Using Python's with Statement

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Leodanis Pozo Ramos

Leodanis is a self-taught Python developer, educator, and technical writer with over 10 years of experience.

» More about Leodanis

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!