Menu
Try in Playground

Python Context Managers: The with Statement, Explained

What the with statement is really doing — automatic cleanup for files, locks, database connections, and anything else that needs a reliable close.

The Statement That Cleans Up After Itself

Every resource you open in a program — a file, a network connection, a database handle, a lock — needs to be closed when you're done. Forget, and you leak memory, hold locks that block other processes, or corrupt files on crash. Python's with statement handles this for you.

The pattern everyone starts with is reading a file:

with open("notes.txt") as f:
    contents = f.read()
    print(contents)

Two things happen automatically. On entry, open() gives you a file object bound to f. On exit — whether the block finishes normally, returns early, or raises — Python calls f.close() for you.

That's it. That's the whole point of with.

What "With" Replaces

Before context managers, the equivalent safe code was a try/finally:

f = open("notes.txt")
try:
    contents = f.read()
    print(contents)
finally:
    f.close()

Five lines of ceremony for "read a file and close it when done." Multiply that by every open in a larger program and you start to see the appeal. with is shorter, harder to get wrong, and impossible to forget the cleanup on.

Opening Multiple Resources

You can bind several context managers in one with:

with open("input.txt") as src, open("output.txt", "w") as dst:
    dst.write(src.read().upper())

Both files open on entry, both close on exit. If the first open succeeds and the second one raises, Python still closes the first — the machinery handles partial setup correctly.

For longer lists of resources, the parentheses form (Python 3.10+) is clearer:

with (
    open("a.txt") as a,
    open("b.txt") as b,
    open("c.txt") as c,
):
    ...

What a Context Manager Actually Is

Any object that defines __enter__ and __exit__ is a context manager. The protocol is dead simple:

  • __enter__(self) runs when the with block starts. Its return value is what as name binds to.
  • __exit__(self, exc_type, exc_value, traceback) runs when the block ends, regardless of how. If an exception caused the exit, the exception info is passed in so the context manager can inspect it or suppress it.

Here's a minimal one that times the block it wraps:

main.py
Output
Click Run to see the output here.

with Timer(): creates the object, calls its __enter__, runs the body, calls __exit__. No file, no lock — just a small wrapper around "do something, measure how long it took."

The contextlib.contextmanager Shortcut

Defining a class for every context manager is heavier than it needs to be. contextlib.contextmanager turns a generator function into a context manager — one yield separates the "before" from the "after":

main.py
Output
Click Run to see the output here.

Everything before yield is the __enter__ behavior. Everything after is the __exit__. The try/finally makes the cleanup run even if the body raises.

Most custom context managers you'll write fit this shape. Reach for the decorator form first; drop to a class only when you need something the generator form can't express.

Temporarily Changing Something

A common shape: set something, use it, restore it. Context managers express this cleanly:

main.py
Output
Click Run to see the output here.

Any "set, then restore" pattern — env vars, logging verbosity, feature flags, test fixtures — fits into a context manager naturally. Callers don't have to remember to restore anything.

Suppressing Exceptions

The __exit__ method can return True to tell Python "I handled the exception; swallow it." That's rare and usually a smell, but it's how contextlib.suppress works:

main.py
Output
Click Run to see the output here.

suppress(FileNotFoundError) turns the FileNotFoundError into a no-op. Use it for genuinely optional operations — "try this, don't care if it doesn't work." Don't use it to silence exceptions you haven't thought about.

Other Context Managers You'll Meet

Context managers show up all over the standard library once you start looking:

import threading
from pathlib import Path

# Locks — guarantee release even if the critical section raises.
lock = threading.Lock()
with lock:
    ...

# tempfile — delete the temp file when you're done.
from tempfile import TemporaryDirectory
with TemporaryDirectory() as tmp:
    path = Path(tmp) / "scratch.txt"
    path.write_text("hello")

# Database connections — close the connection (or end the transaction).
import sqlite3
with sqlite3.connect(":memory:") as conn:
    conn.execute("CREATE TABLE t (x INTEGER)")

Third-party libraries follow the same conventions. When you see with something as x:, it almost always means "use x for the duration of this block, and clean up afterward."

When Not to Use with

  • When you don't actually have setup and teardown. Wrapping arbitrary code in a context manager for no reason adds noise.
  • When you need the resource across many unrelated blocks. Holding a with open for the whole lifetime of a long script can hide what the cleanup scope actually is. Consider a class that owns the resource instead.
  • When a decorator fits better. Some repeated patterns (retry, log, time) read more naturally as @decorator on a function than as with ...: inside it. Pick whichever reads better at the call site.

Most of the time, with is right. The rare exceptions are easy to spot once you're looking for them.

Next: Working With Real Files

You now know the mechanism behind with open(...) as f: — which is the context you'll use it in ninety percent of the time. The next chapter puts it to work reading, writing, and navigating files on disk.

Frequently Asked Questions

What does with open do in Python?

with open(path) as f: opens the file and binds it to f for the duration of the block. When the block ends — normally or because of an exception — Python automatically closes the file. You don't need f.close(); the with statement guarantees it.

Why use with instead of plain open()?

Because with closes the file even when an exception fires halfway through the block. Plain open() leaves you on the hook to remember close() in every code path, including error paths. with is safer and shorter.

How do I open multiple files with one with statement?

Comma-separate the context managers: with open('a.txt') as a, open('b.txt') as b:. Both files are opened on entry and closed on exit, in reverse order. This replaces nested with statements when you need several resources at once.

Learn to code with Coddy

GET STARTED