Python Decorators vs Context Managers: When to Use Each

Decorators wrap a whole function; context managers wrap a block of code. How each one works, a transaction that rolls back on error, a single timer usable as both a decorator and a with statement, suppress and ExitStack from contextlib, and a clear rule for choosing between them.

GloryTecks Team

Training & Placement Faculty

6 May 2026 6 min read

Last updated: 24 September 2026

Share:

Decorators and context managers solve the same kind of problem: running some code before and after other code, such as starting a timer and reporting it, opening a resource and closing it, or beginning a transaction and committing or rolling it back. The difference is scope. A decorator wraps a whole function, every time it is called. A context manager wraps exactly the block of code inside a with statement. Once you think in those terms, most choices make themselves.

A context manager wraps a block

A with statement calls the object's __enter__ method before the block and its __exit__ method after it, however the block ends: normally, through return, or by raising an exception (PEP 343). The easiest way to write one is a generator function with @contextmanager: the code before yield is the setup, the code after it is the clean-up (Python docs: contextlib).

python
import time
from contextlib import contextmanager

@contextmanager
def timer(label):
    start = time.perf_counter()
    try:
        yield
    finally:
        print(f"{label}: {time.perf_counter() - start:.1f}s")

def nightly_job():
    rows = list(range(1000))            # fast preparation, not timed
    with timer("slow section"):
        time.sleep(0.1)                  # only this part is measured
    return len(rows)

print(nightly_job())
text
slow section: 0.1s
1000

The try/finally around yield is what guarantees the report even if the block raises. The key point is precision: the timer measures one section of the function, which a decorator cannot do.

Handling errors inside the block: a transaction

Context managers can react to exceptions. __exit__ receives the exception, if there was one, so it can undo work. Database transactions are the classic case: commit if the block succeeded, roll back if it failed. Python's own sqlite3 connection behaves this way when used in a with statement (Python docs: sqlite3):

python
import sqlite3

db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE accounts (name TEXT PRIMARY KEY, balance INTEGER CHECK (balance >= 0))")
db.executemany("INSERT INTO accounts VALUES (?, ?)", [("asha", 500), ("ravi", 100)])
db.commit()

def transfer(amount):
    try:
        with db:                                   # commit on success, roll back on error
            db.execute("UPDATE accounts SET balance = balance + ? WHERE name = 'ravi'", (amount,))
            db.execute("UPDATE accounts SET balance = balance - ? WHERE name = 'asha'", (amount,))
    except sqlite3.IntegrityError as exc:
        print("rolled back:", exc)
    print(dict(db.execute("SELECT name, balance FROM accounts")))

transfer(200)
transfer(1000)
text
{'asha': 300, 'ravi': 300}
rolled back: CHECK constraint failed: balance >= 0
{'asha': 300, 'ravi': 300}

In the failed transfer, Ravi's balance was already credited when Asha's debit broke the constraint. The rollback undid the credit, so no money appeared from nowhere. The transaction's scope is a few lines, not the whole function, which is exactly what a with block expresses.

A decorator wraps every call

When the behaviour applies to the entire function on every call, such as timing each request, retrying, caching or checking permissions, a decorator is the natural fit, and the rule is stated once at the top instead of inside the body. Python decorators with real examples builds several.

One object, both ways: ContextDecorator

Often you want both: time a whole function sometimes, and one block at other times. Context managers created with @contextmanager are also usable as decorators, because they are built on ContextDecorator (Python docs: contextlib). The timer above already works both ways:

python
@timer("whole export")
def export_all():
    time.sleep(0.1)
    return "done"

print(export_all())
with timer("ad hoc check"):
    time.sleep(0.1)
text
whole export: 0.1s
done
ad hoc check: 0.1s

As a decorator, a fresh context is entered for each call. This is the cleanest answer to "decorator or context manager?" when a behaviour genuinely has both uses: write the context manager, and get the decorator for free.

When to write a class instead

@contextmanager covers most cases. Write a class with __enter__ and __exit__ when the context manager needs state that outlives the block or methods of its own, for example a connection pool object that is also used outside with. Two details matter. Whatever __enter__ returns is what as binds, so return self if the caller should use the object inside the block. And __exit__(exc_type, exc, tb) should normally return None or False, letting any exception continue after the clean-up; returning True swallows it, which is almost never what the caller expects.

Asynchronous code has matching tools: async with calls __aenter__ and __aexit__, and contextlib.asynccontextmanager builds one from an async def generator. Async database sessions and HTTP clients are usually used this way.

Two contextlib helpers worth knowing

suppress states that a specific exception is expected and harmless, more plainly than an empty except block:

python
import os
from contextlib import ExitStack, suppress

with suppress(FileNotFoundError):
    os.remove("old-cache.tmp")          # fine if it was never there
print("continued")
text
continued

ExitStack manages a number of context managers that is only known at run time, such as opening every file in a list, and closes all of them, in reverse order, even if opening one fails halfway:

python
names = ["a.txt", "b.txt", "c.txt"]
for n in names:
    with open(n, "w", encoding="utf-8") as f:
        f.write(n.upper())

with ExitStack() as stack:
    files = [stack.enter_context(open(n, encoding="utf-8")) for n in names]
    print([f.read() for f in files])
print(all(f.closed for f in files))
text
['A.TXT', 'B.TXT', 'C.TXT']
True

Choosing

QuestionDecoratorContext manager
Does the behaviour cover the entire function, on every call?YesPossible, but repetitive
Does it cover only part of a function?NoYes
Is it about a resource (file, lock, connection, transaction)?RarelyYes
Must it react to an exception in the wrapped code?PossibleNatural: __exit__ receives it
Must it change the arguments or the return value?YesNo
Is it used at the call site, by code that does not own the function?NoYes

Two short rules cover most cases. Resources and sections use a with statement. Files, locks, database transactions and temporary settings belong in context managers, because their lifetime is a block of code. Policies on whole functions use a decorator. Caching, retries, timing, registration and access control belong in decorators, because they apply to every call. When something is both, write a context manager with @contextmanager and use it either way.

Can every decorator be written as a context manager?

Not quite. A context manager cannot change a function's arguments or its return value, and it cannot decide to skip the call entirely the way a caching decorator does. Behaviour that only happens before and after the code can be written either way.

What does a with statement do if the block raises?

It still calls __exit__, passing the exception type, value and traceback. If __exit__ returns a true value, the exception is suppressed; otherwise it continues to propagate after clean-up, which is what almost every context manager should do.

Which built-in objects are context managers?

Many: file objects returned by open(), threading locks, database connections, temporary directories from tempfile, and decimal contexts, among others. If an object needs releasing or restoring when you are done, check whether it supports with before writing try and finally by hand.

Do I still need close() when I use with?

No. Leaving the with block closes files and releases locks for you, even when an error occurs. One exception worth knowing: a sqlite3 connection's with block manages the transaction but does not close the connection, so call close() when you are done with it.

Context managers built with @contextmanager are generators underneath; Python generators and iterators made simple explains yield. Clean-up on failure is part of good error handling, covered in error handling in Python. Decorators, closures and context managers are part of the advanced concepts module of the Python Programming course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master Python?

Join GloryTecks’ expert-led Python Programming training in Hyderabad with real-time projects and 100% placement support.

Explore course