Whenever your code opens a file, a network connection, or a database handle, something has to make sure that resource gets closed again — even if an error happens halfway through. Forgetting that cleanup step is one of the most common sources of resource leaks in real programs. Python's with statement solves this with a clean, readable syntax that guarantees cleanup runs no matter what.

A context manager is any object that knows how to set itself up and tear itself down automatically. The with statement is simply the syntax Python gives you to use one.

The Problem Without with

Without a context manager, you'd normally write something like this:

f = open("data.txt", "r")
try:
    content = f.read()
finally:
    f.close()  # must remember this, even if read() raises an error

It works, but it's verbose, and it's easy to forget the finally block — which means the file handle leaks if an exception is raised before f.close() runs.

The with Statement

The built-in open() function returns a context manager, so you can replace the whole block above with:

with open("data.txt", "r") as f:
    content = f.read()

print("File is now closed:", f.closed)

Output:

File is now closed: True

As soon as the indented block under with finishes — normally or because of an exception — Python automatically calls the file's cleanup code. You never have to remember to call close() yourself, and it's guaranteed to run even if f.read() throws an error.

Multiple Context Managers

You can open more than one resource in a single with statement, separated by commas:

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

Both files are closed automatically when the block exits, in the correct order, even if an exception happens while writing.

Writing Your Own Context Manager

You're not limited to files — database connections, locks, timers, and your own custom resources can all become context managers. There are two ways to build one: a class implementing __enter__ and __exit__, or a generator-based function decorated with @contextmanager from the contextlib module. Both approaches, with full working code, are covered step by step in our Python context manager program example.

The key thing to remember is this: if you find yourself writing a try/finally block just to guarantee some cleanup code runs, that's almost always a sign you should reach for with instead — either using a built-in context manager or writing your own.