A context manager in Python sets something up, gives it to you inside a with block, and guarantees the cleanup afterwards, even if an error happens. The most familiar one is with open("file.txt") as f:, which closes the file for you. You can also build your own with a class (__enter__ and __exit__) or with the @contextmanager decorator.
This guide explains why with exists, how it works under the hood, and how to write your own, with real output for every example.
In this guide
The short version
with thing as name:runs setup, runs your block, then always runs cleanup.- Cleanup happens even if the block raises an exception.
- Build your own with
__enter__and__exit__, or with a generator and@contextmanager. - Use it for files, locks, database connections, temporary changes and timers.
The problem with (not) cleaning up
Some resources must be released after use: files should be closed, connections disconnected, locks released. Forgetting causes leaks and bugs. The safe manual way is a try with a finally, and it works, but it is long and easy to get wrong. The with statement does the same thing in fewer lines:
import io
buf = io.StringIO()
try:
buf.write("data")
finally:
buf.close()
print(buf.closed)
with io.StringIO() as buf2:
buf2.write("data")
print(buf2.closed)
Output
True
True
Both versions ended with the buffer closed. If you are new to finally, see Python Exception Handling: try, except, finally.
The with statement
The most common use is with files. The file is open inside the block and closed the moment the block ends, so you never call close() yourself. The example below also uses a temporary folder, which is itself a context manager that deletes the folder when the block ends:
import os, tempfile
with tempfile.TemporaryDirectory() as folder:
path = os.path.join(folder, "note.txt")
with open(path, "w") as f:
print(f.closed)
f.write("hello")
print(f.closed)
with open(path) as f:
print(f.read())
inside = os.path.exists(folder)
print(inside, os.path.exists(folder))
Output
False
True
hello
True False
Follow the output: False (open inside the block), True (closed after it), the text we wrote, and finally the temporary folder existing inside its block and being gone afterwards. Reading and writing files is covered in Reading and Writing Files in Python.
The cleanup runs even when something goes wrong inside the block:
import io
try:
with io.StringIO() as buf:
buf.write("data")
raise ValueError("oops")
except ValueError:
print("caught")
print(buf.closed)
Output
caught
True
An error was raised inside the block, and the buffer was still closed before the exception reached the except.
How it works: __enter__ and __exit__
Any object with two special methods is a context manager. __enter__ runs at the start of the with block, and its return value is what as receives. __exit__ runs at the end, always, and is told whether an exception occurred:
class Timer:
def __enter__(self):
print("start")
return self
def __exit__(self, exc_type, exc, tb):
print("stop", exc_type.__name__ if exc_type else "no error")
return False
with Timer() as t:
print("working")
try:
with Timer():
raise ValueError("bad")
except ValueError:
print("error propagated")
Output
start
working
stop no error
start
stop ValueError
error propagated
In the second run, __exit__ received the ValueError, printed it, returned False, and the exception carried on to the outside world. This is what “guaranteed cleanup” means in practice.
The three parameters of __exit__ describe the exception (its type, value and traceback), and are all None when the block finished normally.
Suppressing exceptions
If __exit__ returns True, the exception is swallowed: the block stops, but the program continues after the with statement as if nothing happened:
class Ignore:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return True
with Ignore():
1 / 0
print("continued")
Output
continued
Be careful with this: it hides errors. When you do want to ignore one specific, expected exception, the standard library has a neat tool, contextlib.suppress:
from contextlib import suppress
with suppress(KeyError):
{}["missing"]
print("still running")
Output
still running
The easy way: @contextmanager
Writing a class for every context manager is heavy. contextlib.contextmanager lets you write a generator instead. Everything before yield is the setup, the yielded value is what as receives, and everything after is the cleanup:
from contextlib import contextmanager
@contextmanager
def announce(name):
print("enter", name)
try:
yield name.upper()
finally:
print("exit", name)
with announce("db") as value:
print("using", value)
Output
enter db
using DB
exit db
Wrap the yield in try / finally, as above. If you do not, an exception in the with block skips your cleanup code:
from contextlib import contextmanager
@contextmanager
def bad():
print("setup")
yield
print("cleanup")
try:
with bad():
raise ValueError("boom")
except ValueError:
print("error")
Output
setup
error
“cleanup” never printed. That is the most common bug when writing these. (Generators themselves are explained in Python Iterators and Generators Explained.)
Useful built-in context managers
| Context manager | What it does |
|---|---|
| open(path) | Opens a file and closes it afterwards |
| tempfile.TemporaryDirectory() | Makes a temporary folder and removes it afterwards |
| contextlib.suppress(Error) | Ignores one expected kind of exception |
| contextlib.redirect_stdout(target) | Sends print output somewhere else for a while |
| threading.Lock() | Holds a lock for the length of the block |
| decimal.localcontext() | Temporarily changes decimal precision |
| unittest.mock.patch(…) | Temporarily replaces something while testing |
For example, capturing what a piece of code prints:
import io
from contextlib import redirect_stdout
buf = io.StringIO()
with redirect_stdout(buf):
print("captured")
print(repr(buf.getvalue()))
Output
'captured\n'
You can open several context managers in one with by separating them with commas:
import io
with io.StringIO("a\nb") as src, io.StringIO() as dst:
for line in src:
dst.write(line.strip().upper())
print(dst.getvalue())
Output
AB
Common mistakes
Mistake 1: using the resource after the block
Once the block ends, the resource is closed and cannot be used any more:
import io
buf = io.StringIO()
buf.close()
try:
buf.write("late")
except ValueError as e:
print(type(e).__name__, e)
Output
ValueError I/O operation on closed file
Mistake 2: forgetting to return the object from __enter__
The value after as is whatever __enter__ returns. If you forget, you get None:
class Res:
def __enter__(self):
pass
def __exit__(self, *args):
pass
with Res() as r:
print(r)
Output
None
Mistake 3: skipping try/finally inside @contextmanager
Shown above: without it the cleanup is skipped when the block fails.
Mistake 4: returning True from __exit__ by accident
That silently hides every exception from the block. Return True only when you deliberately mean to swallow one.
Try it yourself
Work out each answer first, then open the solution.
1. Write a context manager class that prints <tag> when the block starts and </tag> when it ends.
Show solution
class Tag:
def __enter__(self):
print("<tag>")
def __exit__(self, *args):
print("</tag>")
with Tag():
print("content")
Output
<tag>
content
</tag>2. What does this print?
from contextlib import suppress
with suppress(ZeroDivisionError):
print("a")
print(1 / 0)
print("b")
print("c")
Show answer
Output
a
cThe error stops the block at the division, so “b” never prints. suppress swallows it, and the program continues with “c”.
3. Is the object closed after with io.StringIO() as f: pass?
Show answer
import io
with io.StringIO() as f:
pass
print(f.closed)
Output
True4. What does this print?
class C:
def __enter__(self):
print("in")
return 5
def __exit__(self, *args):
print("out")
with C() as v:
print(v + 1)
Show answer
Output
in
6
out__enter__ printed “in” and returned 5, which became v. The block printed 6, then __exit__ printed “out”.
Try them in our free Python compiler.
Frequently asked questions
What is a context manager in Python?
It is an object that defines setup and cleanup around a block of code, used with the with statement. Files, locks and database connections are typical examples.
Why use with open() instead of open() and close()?
with closes the file automatically, even if an error occurs while you are using it. With a manual close(), an exception can skip it and leave the file open.
Does the with statement catch exceptions?
Not by itself. The exception still propagates after the cleanup runs, unless the context manager’s __exit__ returns True to suppress it.
How do I create my own context manager?
Either write a class with __enter__ and __exit__ methods, or write a generator function with a single yield and decorate it with @contextlib.contextmanager.
Can I use more than one context manager in a single with?
Yes. Separate them with commas: with open(a) as x, open(b) as y:. They are entered in order and exited in reverse order.
What is async with?
It is the asynchronous version, used with async code and objects that define __aenter__ and __aexit__. The idea is the same: guaranteed setup and cleanup.
Related reading
- Python Exception Handling: try, except, finally – the try/finally that with replaces.
- Reading and Writing Files in Python – the most common context manager.
- Python Iterators and Generators Explained – how @contextmanager works.
- Python Decorators Explained Step by Step – the @ syntax used above.
Run this code in your browser
The free Upskly compiler runs Python with nothing to install. Paste the example, change it and see what happens.
Stuck on a traceback?
AI Assist works inside the Python notebook, so you can ask about an error or a concept without leaving the cell.
Test yourself
Timed questions on Exceptions and Context Managers, with an explanation for every answer.