Cleanup is the bookkeeping your program does to leave the world in a consistent state: closing files, releasing locks, rolling back transactions, deleting temporary data. If cleanup is skipped when an exception is raised, small leaks accumulate into bigger problems — locked files on Windows, hanging database connections, half-written outputs. Python offers two clean ways to attach cleanup to code: the finally clause and the with statement.
The finally block runs after try and any except/else, whether the body completed normally, returned early, or raised. It is the right place for “no matter what, do this afterwards” code. It is also the simplest error-safety tool: pair a resource acquisition with a matching release inside the same try/finally.
The with statement is a higher-level form of the same idea. Any object that defines __enter__ and __exit__ (a context manager) can be used with with, and its cleanup is executed on exit automatically, even during exceptions. Files, locks, temporary directories, database connections and many other resources ship their own context managers.
For ad-hoc cleanup where you don't want to write a whole class, contextlib.contextmanager turns a generator function into a context manager in three lines. contextlib.ExitStack lets you enter an arbitrary number of context managers at runtime and clean them all up together, which is invaluable when the number of resources is dynamic.
try/finally: the primitive
The simplest pattern is try: resource = acquire(); do_work(resource) finally: resource.release(). It is verbose compared to with, but it works for any cleanup, including ones that depend on variables defined in the try body.
finally runs even on return, break, continue, and raise. If finally itself raises, the new exception replaces the original. Keep finally bodies simple to avoid losing the first error.
with and contextlib
with open("f.txt") as f: is the canonical example. Most library resources follow the same convention: with threading.Lock() as lock, with sqlite3.connect(db) as conn, with tempfile.TemporaryDirectory() as tmp.
contextlib.contextmanager lets you write @contextmanager def my_ctx(): try: yield x finally: cleanup(). Handy for a one-off setup/teardown pair you want to use in several call sites.
Tools for attaching cleanup to blocks of code.
| Tool | Purpose |
|---|---|
try / finallystatement | Runs cleanup even on early return or exception. |
with ctx as xstatement | Automatic setup/teardown via context managers. |
@contextmanagerdecorator | Turns a generator into a context manager. |
ExitStackclass | Register an arbitrary number of context managers. |
contextlib.suppress(E)class | Ignore a specific exception as a context manager. |
TemporaryDirectoryclass | Directory that is removed on context exit. |
threading.Lock()class | Lock with automatic release on with exit. |
sqlite3.Connectionclass | Commits or rolls back based on with exit. |
Cleanup Actions During Error Handling code example
The script shows three cleanup patterns — try/finally, with, and @contextmanager — side by side on a small resource.
# Lesson: Cleanup Actions During Error Handling
from contextlib import contextmanager, ExitStack
from pathlib import Path
from tempfile import TemporaryDirectory
class Connection:
def __init__(self, name: str): self.name = name; self.open = True
def close(self): self.open = False; print(f" closed {self.name}")
# Pattern 1: try/finally
def run_1():
conn = Connection("A")
try:
print(" using A")
raise RuntimeError("boom in A")
finally:
conn.close()
try:
run_1()
except RuntimeError as err:
print("caught:", err)
# Pattern 2: @contextmanager
@contextmanager
def open_conn(name: str):
conn = Connection(name)
try:
yield conn
finally:
conn.close()
try:
with open_conn("B") as c:
print(" using", c.name)
raise RuntimeError("boom in B")
except RuntimeError as err:
print("caught:", err)
# Pattern 3: ExitStack for dynamic resource count
with ExitStack() as stack:
conns = [stack.enter_context(open_conn(n)) for n in ("C", "D", "E")]
print(" opened:", [c.name for c in conns])
# Built-in cleanup: TemporaryDirectory
with TemporaryDirectory() as tmp:
p = Path(tmp) / "note.txt"
p.write_text("hello", encoding="utf-8")
print(" tmp file exists?", p.exists())
print("tmp still exists?", Path(tmp).exists())
Pay attention to the order of prints:
1) Pattern 1: try/finally always closes, even when a RuntimeError escapes.
2) Pattern 2: @contextmanager hides the try/finally inside a reusable helper.
3) Pattern 3: ExitStack handles an arbitrary number of resources uniformly.
4) TemporaryDirectory deletes itself on exit — `Path(tmp).exists()` is False afterwards.
Write a one-shot helper with @contextmanager and wrap it around a file.
from contextlib import contextmanager
from pathlib import Path
from tempfile import gettempdir
@contextmanager
def writing(path: Path):
f = open(path, "w", encoding="utf-8")
try:
yield f
finally:
f.close()
print("closed", path.name)
p = Path(gettempdir()) / "note.txt"
with writing(p) as f:
f.write("hello\n")
print(p.read_text(encoding="utf-8"))
p.unlink()
Confirm the order of cleanup.
from contextlib import contextmanager
calls = []
@contextmanager
def step(name):
calls.append(f"in:{name}")
try: yield
finally: calls.append(f"out:{name}")
with step("A"), step("B"):
calls.append("body")
assert calls == ["in:A", "in:B", "body", "out:B", "out:A"]
Running the script prints (order matters):
using A
closed A
caught: boom in A
using B
closed B
caught: boom in B
opened: ['C', 'D', 'E']
closed E
closed D
closed C
tmp file exists? True
tmp still exists? False