Decorators and Context Managers
Intermediate⏱80 minutesπŸ“–17 min readLesson 6 of 10

Decorators and Context Managers

Wrap behaviour without rewriting it: write decorators that keep their metadata, stack them in the right order, build context managers that release resources on every path, and manage a dynamic set of resources with ExitStack

The billing service had a clean function that charged a card. Then the team added a retry, a timer, an audit log and a permission check. Each one arrived as a separate copy of the same wrapper, pasted around a different call site. After a year the charge function had four nested try blocks, three retry policies and a log line that printed the wrong function name, because every wrapper had forgotten to copy the original.

Decorators and context managers add behaviour without editing the function that provides it. A decorator wraps a callable, so logging, timing, caching and retry logic live in one place. A context manager wraps a block, so opening, closing and rolling back a resource happen on every exit path. Both rest on one idea: functions are objects, and an object can be passed, stored and replaced.

This lesson explains what the @ syntax assigns, why metadata has to be copied by hand, why a decorator with arguments has three layers, and how __exit__ decides whether an exception is suppressed.

Key terms

First-class function
A function that can be assigned to a name, passed as an argument and returned from another function like any other object.
Closure
A nested function that keeps a reference to the variables of the enclosing scope where it was defined.
Decorator
A callable that receives a function and returns a replacement function, applied with the @ syntax at definition time.
Wrapper
The inner function a decorator returns, which usually calls the original function and adds behaviour around it.
Context manager
An object that implements __enter__ and __exit__ so a with statement can set up and tear down a resource.
Exit method
The __exit__ method, which receives the exception details and returns a true value to suppress the exception.
Generator context manager
A context manager built by @contextmanager from a generator that yields exactly once between setup and cleanup.
ExitStack
A context manager that keeps a stack of cleanup callbacks and runs them in reverse order when the block ends.
Metadata
The attributes of a function such as __name__, __doc__ and the signature that tools and tracebacks read.
Reentrant
A context manager that can be entered again while an earlier use is still active, such as suppress or redirect_stdout.

1. Functions Are Objects

A function is an object with attributes. It can be assigned to another name, stored in a list, passed to a function and returned from one. The call syntax name(...) operates on that object, not on the name.

python
def square(value: int) -> int: return value * value operation = square print(operation.__name__) print(operation(6)) print([callable(item) for item in (square, 42)])

The output is square, then 36, then [True, False]. Both names refer to the same function object, and callable reports whether an object can be called. That is the foundation of the decorator protocol: if a function can be passed as an argument, another function can accept it and return a replacement.

A nested function can also read a name from the enclosing scope after that scope has returned, because the name is captured in the function object. That is a closure, and the wrapper in the next section is one: it keeps func alive long after the decorator returns.

ℹ️Note

A closure captures the name, not the value at definition time. The late binding trap from lesson 5, where every lambda returned 2, is the same mechanism seen from the other side.

2. The Decorator Protocol

A decorator is a callable that takes a function and returns a replacement. The @ syntax is shorthand for a call followed by an assignment. The two snippets below are equivalent, and the second is what the interpreter actually does.

python
def logger(func): def wrapper(*args, **kwargs): print(f"calling {func.__name__}") return func(*args, **kwargs) return wrapper def add(a: int, b: int) -> int: return a + b add = logger(add) print(add(3, 5))

The output is calling add and then 8. Writing @logger above def add produces the same object without repeating the name three times. Decoration happens once, when the def statement runs at import. The wrapper body runs on every call.

100%
Description

A flowchart of what happens when a decorated function is defined. The def statement creates the original function object, the decorator receives it and returns a wrapper that closes over it, and the original name is rebound to the wrapper. At call time the wrapper runs its prefix, calls the original function, runs its suffix, and returns the result.

The *args, **kwargs signature is what makes a wrapper general. Without it the wrapper would accept only the arguments it names, and decorating a function with a different signature would fail at call time. The wrapper forwards what it received and returns what the original returned.

πŸ“ŒImportant

A decorator runs at import time and its wrapper runs at call time. Work that should happen once, such as reading a configuration value or validating a registration, belongs in the decorator body. Work that should happen per call belongs in the wrapper.

A decorator does not have to call the original at all. It can replace it, register it and return it unchanged, or wrap it several times. The protocol only requires a callable in and a callable out, which is why the standard library builds @property, @staticmethod, @functools.cache and @dataclass on one mechanism.

3. Metadata, wraps and Introspection

The wrapper above has a problem that no test will catch. Its name is wrapper, its docstring is None, and its signature is (*args, **kwargs). Every tool that reads those attributes now describes the wrong function.

python
import inspect def plain_logger(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @plain_logger def greet(name: str, punctuation: str = "!") -> str: """Greet someone politely.""" return f"Hello, {name}{punctuation}" print(greet.__name__) print(greet.__doc__) print(inspect.signature(greet))

The output is wrapper, then None, then (*args, **kwargs). A traceback from inside greet also points at wrapper, and the help page shows no parameters, so the function is harder to use without being broken.

functools.wraps fixes the metadata by copying it from the original. It is a decorator factory for update_wrapper, which assigns __module__, __name__, __qualname__, __annotations__, __type_params__ and __doc__, updates __dict__, and adds a __wrapped__ attribute that points at the original.

python
import functools import inspect def logger(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @logger def greet(name: str, punctuation: str = "!") -> str: """Greet someone politely.""" return f"Hello, {name}{punctuation}" print(greet.__name__) print(greet.__doc__) print(inspect.signature(greet)) print(greet.__wrapped__.__name__)

The output is greet, then Greet someone politely., then (name: str, punctuation: str = '!') -> str, then greet. The signature is correct because inspect.signature follows __wrapped__ by default, and passing follow_wrapped=False shows the wrapper's real signature when a tool needs it.

The video below writes a decorator from scratch and shows the difference that functools.wraps makes to the function name and the help output.

Corey SchaferWatch on YouTube
Description

A video lesson that defines a decorator, applies it with the at syntax, and then shows how functools.wraps restores the original function name, docstring and signature.

⚠️Warning

A wrapper without functools.wraps breaks more than documentation. Frameworks that dispatch on __name__, caches keyed by the function object, and test reports that list test names all read the copied attributes. Add wraps in the first version, not after a bug report.

4. Decorators With Arguments and Decorators as Objects

@repeat(times=3) is a different shape from @logger. The parentheses mean the expression after the @ is evaluated first, and its result is used as the decorator. That adds a third layer: an outer factory that stores the arguments, a middle decorator that receives the function, and an inner wrapper that runs per call.

python
import functools def repeat(times: int = 2): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): result = None for _ in range(times): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(times=3) def announce(message: str) -> str: return message.upper() print(announce("ready"))

The output is READY, and the function ran three times. repeat(3) returns decorator, decorator(announce) returns wrapper, and the name announce is bound to that wrapper.

The trap is that a factory used without parentheses silently decorates nothing useful. @repeat passes the function itself as times and returns decorator, so the name ends up bound to the middle layer. The first call then fails with a TypeError, far from the mistake, because decorator expects a function.

The fill in the blank exercise checks the three layers and their responsibilities.

Fill in the Blanks
How to answer

Fill-in-the-blank exercise with 4 blanks. Type the missing words, then check your answer.

Complete the description of a decorator that takes arguments.

The outer factory receives the blank and returns a decorator. The middle layer receives the blank and returns a wrapper. The inner wrapper runs on every blank, calls the original function, and returns its blank.

Word Bank, drag words into the blanks above:

arguments
function
call
result
module
class
import
signature

A decorator can also be an instance of a class that implements __call__. That form is useful when the wrapper needs state between calls, such as a counter or a cache.

python
import functools class CountCalls: def __init__(self, func): functools.update_wrapper(self, func) self.func = func self.count = 0 def __call__(self, *args, **kwargs): self.count += 1 return self.func(*args, **kwargs) @CountCalls def say(message: str) -> None: print(message) say("hi") say("bye") print(say.count, say.__name__)

The output is hi, bye, then 2 say. update_wrapper copies the metadata onto the instance, which works because a normal instance has a writable __dict__. Without it, say.__name__ would raise AttributeError, because an instance does not inherit the wrapped function's name.

The instance is created once, at decoration time, so say.count is shared by every call. That is the point and also the hazard. A decorated function called from several threads needs a lock around the counter, and a decorated function used as a dictionary key is keyed by the instance rather than the original.

πŸ’‘Tip

Name the layers. Use decorator for the middle function and wrapper for the inner one, and keep the factory body free of work that should happen per call.

5. Stacking Decorators and Their Order

Several decorators can be stacked on one function. They are applied from the bottom up, because the syntax is repeated function application, and they run from the outside in at call time.

python
import functools trace = [] def mark(label): def decorator(func): trace.append(f"{label} applied") @functools.wraps(func) def wrapper(*args, **kwargs): trace.append(f"{label} before") result = func(*args, **kwargs) trace.append(f"{label} after") return result return wrapper return decorator @mark("outer") @mark("inner") def target(): trace.append("target body") target() print(trace)

The output is ['inner applied', 'outer applied', 'outer before', 'inner before', 'target body', 'inner after', 'outer after']. Application runs bottom up, so inner wraps the function first and outer wraps the result. Calls enter the outermost wrapper first, which is why outer before appears before inner before.

100%
Description

A sequence diagram of two stacked decorators. The caller calls the outer wrapper, which logs its prefix and calls the inner wrapper, which logs its prefix and calls the original function. The original returns to the inner wrapper, which logs its suffix, then the outer wrapper logs its suffix and returns to the caller.

Order changes behaviour when the layers are not symmetric. A permission check should sit outside a cache, so an unauthorised caller never reads a cached value. A retry should sit outside a timer, so the timer measures each attempt. A log decorator should sit outside everything else to record inner failures.

Stacking is not free. Each layer is another call, another frame and a little more latency. The chart below measures a one line function called two million times per configuration on CPython 3.14.4.

Call overhead of stacked decorator layers
Description

Bar chart of measured call latency for a trivial function with zero, one, two and four wrapper layers. The undecorated call takes about 39 nanoseconds, one layer about 136, two layers about 231 and four layers about 358.

The numbers are specific to that interpreter and machine, so treat the shape as the lesson rather than the absolute values. A wrapper costs roughly one hundred nanoseconds here, which is irrelevant around a network call and significant inside a tight numeric loop. Measure before adding a fourth layer to a hot path.

πŸ“ŒImportant

Read a stack from the outside in when reasoning about behaviour, and from the bottom up when reasoning about construction. The decorator written closest to def is applied first and called last.

6. The with Protocol

A context manager is an object with two methods. __enter__ runs when the with block starts and its return value is bound by the as clause. __exit__ runs when the block ends, receives the exception details, and returns a true value to suppress the exception. The statement was added in Python 2.5 to replace the manual try/finally pattern.

python
class Tracker: def __init__(self, name: str, swallow: bool = False) -> None: self.name = name self.swallow = swallow def __enter__(self): print("enter", self.name) return self def __exit__(self, exc_type, exc, tb): print("exit", self.name, exc_type.__name__ if exc_type else None) return self.swallow with Tracker("plain") as tracker: print("body", tracker.name) try: with Tracker("quiet", swallow=True): raise ValueError("swallowed") print("after the quiet block") except ValueError: print("not suppressed")

The output begins enter plain, body plain, exit plain None. Then the quiet block prints enter quiet and exit quiet ValueError, the exception is suppressed, and after the quiet block is printed. The try never sees it.

100%
Description

A state diagram of a with statement. The context manager is created, enter runs and binds the as target, the block body runs, and exit runs on every path. Exit returns a false value and the exception propagates, or returns true and execution continues after the block.

The three arguments to __exit__ are None when no exception occurred. When one did occur, exc_type is the class, exc is the instance and tb is the traceback. Returning None or False lets the exception continue, and returning True stops it.

Situation
__exit__ arguments
Return value
Result
Block completed
None, None, None
Ignored
Execution continues
Block raised
Class, instance, traceback
Falsy
Exception propagates
Block raised
Class, instance, traceback
Truthy
Exception suppressed

contextlib.suppress is the smallest useful context manager, and the honest way to say that a specific failure is expected. It is reentrant, so nested uses work.

python
import contextlib with contextlib.suppress(FileNotFoundError): open("/srv/reports/missing.csv") print("survived")

The output is survived. A KeyError from the same block would still propagate, because only the named classes are suppressed. contextlib.nullcontext is the opposite tool: it does nothing and returns its argument, which lets an optional context manager be used without an if around the with.

🚨Danger

Returning True from __exit__ hides the exception from every caller above the block. Use that power for a named, expected failure such as a missing optional file, not as a blanket way to make a red test turn green.

7. @contextmanager: A Generator Becomes a Context Manager

Writing a class for every short lived context is verbose. contextlib.contextmanager turns a generator with exactly one yield into a context manager. Everything before the yield is __enter__, and everything after it is __exit__. If the block raises, the exception is raised at the yield inside the generator, so a try/finally around the yield is the cleanup path.

python
from contextlib import contextmanager @contextmanager def managed_file(path: str, mode: str = "r"): handle = open(path, mode, encoding="utf-8") try: yield handle finally: handle.close() with managed_file("/tmp/opencode/hello.txt", "w") as handle: handle.write("shorter")

The function is a generator, so calling it does not open the file. The with statement drives it: __enter__ calls next, which runs to the yield and returns the handle, then __exit__ resumes it after the yield and closes the file. If the body raises, the exception is thrown in at the yield instead.

The try/finally is not optional. Without it, an exception in the body leaves the generator suspended at the yield, and the cleanup line after it never runs. The file then stays open until the generator is collected, which is not deterministic, and a long running process can exhaust its descriptor limit first.

100%
Description

A flowchart contrasting a correct and an unsafe generator context manager. In both, enter runs to the yield and the block executes. On an exception, the exception is thrown at the yield. With a try finally the cleanup runs and the exception propagates, without it the cleanup is skipped and the resource leaks.

A generator context manager instance is single use. The helper discards the stored call arguments after the first __enter__, so a second with on the same object fails. Create a fresh instance in the with header, which is the documented pattern.

@contextmanager also returns an object that inherits from ContextDecorator, so the same generator can decorate a function. In the decorator form the helper rebuilds the context manager on every call, which is why the single use rule does not apply there.

The matching exercise below connects each part of the protocol to its role.

Matching Question
How to answer

Matching exercise with 5 pairs. Match each item on the left with its answer on the right.

Match each piece of the context manager protocol to what it does.

__enter__
Drop here
__exit__ with None
Drop here
__exit__ returning True
Drop here
yield before the body
Drop here
yield inside try/finally
Drop here

Drag answers from here:

Runs setup and its return value is bound by the as clause
Runs cleanup when the block succeeded
Suppresses the exception raised by the block
Marks the setup half of a generator context manager
Guarantees cleanup when the block raises
⚠️Warning

Catch an exception inside a generator context manager only to log it or to translate it. If the generator swallows the exception and does not re-raise it, __exit__ reports success and the with statement continues as if nothing failed.

8. ExitStack and Dynamic Resources

Sometimes the number of resources is not known until the block runs. A function may need to open one file or twenty, or acquire a lock only when a flag is set. contextlib.ExitStack keeps a stack of cleanup callbacks and unwinds it in reverse order, which is the behaviour of nested with statements built at runtime.

python
import contextlib with contextlib.ExitStack() as stack: stack.callback(print, "first registered") stack.callback(print, "second registered") print("body") print("after the stack")

The output is body, second registered, first registered, after the stack. Registration order is top to bottom and cleanup order is bottom to top, which mirrors nested with blocks.

enter_context is the method for a real context manager. It calls __enter__, registers __exit__, and returns the value that as would have bound. If a later resource fails to open, the ones already registered are still released, which is the all or nothing property that a loop of separate with blocks does not give you.

The drag and drop exercise orders the events of a transaction context manager that runs cleanup on every exit path.

Ordering Question
How to answer

Ordering exercise with 5 items. Move them into the correct order, then check your answer.

Order the events from the start of the with block to the end, assuming the body raises.

Enter the context manager and begin the transaction
Run the first statement of the body
Run the statement that raises
Roll back the transaction
Re-raise the original exception
πŸ’‘Tip

Use ExitStack.callback for a plain cleanup function, enter_context for a context manager, and pop_all when the resources must be transferred to a longer lived object instead of released at the end of the block.

9. Real-World Case Studies

Two production shapes combine this lesson with earlier ones: a retry decorator that classifies failures, and a transaction context manager that must not lie about success.

A retry decorator that only retries what can succeed

A retry belongs in a decorator because it is a policy about a call, not a part of the call. The version below is the exception policy from lesson 3, expressed as a wrapper with a configurable budget and a wait hook that a test can replace.

python
import functools import time class TransientError(Exception): """A failure that may succeed if the operation runs again.""" def retry(attempts: int = 3, base_delay: float = 0.5, wait=time.sleep): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, attempts + 1): try: return func(*args, **kwargs) except TransientError: if attempt == attempts: raise wait(base_delay * 2 ** (attempt - 1)) return wrapper return decorator calls = {"count": 0} @retry(attempts=3, wait=lambda delay: print(f"waiting {delay:.1f}s")) def fetch(record_id: int) -> str: calls["count"] += 1 if calls["count"] < 3: raise TransientError("temporary") return f"record {record_id}" print(fetch(7))

The output is waiting 0.5s, waiting 1.0s, then record 7. The wrapper retries only TransientError, so a ValueError fails on the first attempt. When the budget runs out, the bare raise preserves the original traceback instead of replacing it with a generic timeout. The wait hook is a parameter rather than a hard-coded time.sleep, which keeps the test deterministic.

A transaction context manager that rolls back and re-raises

A transaction must commit only when the block succeeds, and it must roll back and re-raise when the block fails. The context manager below makes that contract explicit and keeps the call site free of try blocks.

python
import contextlib events: list[str] = [] @contextlib.contextmanager def transaction(log): log.append("begin") try: yield log except BaseException: log.append("rollback") raise else: log.append("commit") with transaction(events): events.append("insert") print(events)

The output is ['begin', 'insert', 'commit']. When the body raises, the except branch rolls back and the bare raise sends the original exception outward, so the caller sees the real failure and the log stays consistent. A real database exposes begin, commit and rollback methods, and the shape of the context manager is the same.

The except BaseException deserves a comment. It catches KeyboardInterrupt as well, which is deliberate here: an interrupted transaction must still roll back before the process dies. The handler re-raises immediately, so it does not swallow the interrupt; it only guarantees the rollback runs first. A lock is the other classic case, and threading.Lock is a context manager, so the release happens on every exit path.

πŸ“ŒImportant

A decorator that caches results must be placed outside the retry only if the retry result is worth caching. A cache outside a retry stores the first success, while a retry outside a cache repeats a cached failure. Write the stack in the order you can defend in review.

10. Choosing Between Them, Pitfalls and a Review Checklist

Both features wrap something, and the choice is about what is being wrapped. A decorator wraps a callable and runs once per call. A context manager wraps a block and runs once per with, and it can hold state only for that block.

100%
Description

A quadrant chart placing four wrapping tools by scope and state. A plain function decorator sits in the per call, low state corner. A class based decorator with a counter sits in the per call, high state corner. A generator context manager sits in the per block, low state corner. A class based context manager sits in the per block, high state corner.

The table below collects the failures that appear most often in review.

Pitfall
Symptom
Fix
Wrapper without wraps
Tracebacks and help show wrapper
Add @functools.wraps(func)
Factory used without parentheses
TypeError at first call
Use @repeat(times=3)
Stack written in the wrong order
Auth checked after the cache
Reason from the outside in
__exit__ returns True everywhere
Failures disappear silently
Return True only for expected errors
@contextmanager without try/finally
Resource leaks on exception
Wrap the yield in try/finally
Context manager instance reused
Second with fails
Create a new instance per use
Cleanup registered after the work
Resource leaks when setup fails
Register each resource as it is acquired
Decorator body does work per call
Slow import, stale configuration
Move it into the wrapper or the factory

The checklist below is short enough to use on a diff.

  • Does every wrapper call functools.wraps on the function it replaces?
  • Does the wrapper forward *args, **kwargs and return the original result?
  • Is the decorator written with the parentheses its factory requires?
  • Is the stack order defensible for permissions, caching, retry and timing?
  • Does every __exit__ return False unless suppression is the point?
  • Does every generator context manager wrap its yield in try/finally?
  • Is every resource registered as soon as it is acquired, not at the end?
  • Are dynamic resources managed by ExitStack rather than a loop of with blocks?
ℹ️Note

A decorator changes the identity of a function. Anything that compares callables, stores them in a dictionary, or pickles them sees the wrapper, not the original. functools.wraps keeps the metadata but not the identity, so use __wrapped__ when the original is needed.

2026 Updates (as of November 2026)

  • Python 3.15.2 is the current stable documentation set.
  • Since Python 3.15, decorating a generator or coroutine function with a ContextDecorator keeps the context manager open across iteration or await, instead of closing it when the object is created.
  • ExitStack.enter_context and push accept arbitrary descriptors since Python 3.15, so objects that expose __enter__ and __exit__ through a descriptor work without a wrapper.
  • functools.update_wrapper has copied __type_params__ since Python 3.12, alongside __name__, __qualname__, __annotations__ and __doc__, which keeps generic functions readable through a decorator.
  • contextlib.chdir has been available since Python 3.11 and restores the previous working directory on exit, which replaces the hand written save and restore pattern.

πŸ“š Did you know?

  • @functools.cache returns the same kind of wrapper this lesson builds, and its cache_info and cache_clear attributes are the reason a class-based decorator sometimes beats a closure.
  • A decorator can be applied to a class as well as a function. @dataclass and @total_ordering receive the class object and return a modified class, which is the same protocol applied to a different kind of object.
πŸ“ŒImportant

Comparative Verdict. Use a decorator when the behaviour belongs to a call and should be reusable across functions: logging, timing, caching, retry and permission checks. Use a context manager when the behaviour belongs to a region of code and must run on every exit path: files, locks and transactions. Use a class when the wrapper needs inspectable state, a closure when it does not, and ExitStack when the number of resources is decided at runtime. Keep the wrapper transparent: copy the metadata, return the result, re-raise what you cannot repair.

Practice Questions

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A module defines a function with @logger above def add. Which statement describes what the interpreter does with that decoration?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A decorator returns a wrapper without functools.wraps. Which observable problem appears first?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A factory is defined as def repeat(times=2) that returns a decorator. A developer writes @repeat without parentheses. What happens?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A function is decorated with @outer above @inner. Which order describes application and execution?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A class-based decorator stores a call counter and is applied to a function that several threads call. What must the author consider?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A with block raises ValueError and the context manager's __exit__ returns True. What does the code after the with block observe?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A generator context manager is written with a yield and a close call after it, with no try/finally. The with body raises. What is the consequence?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A function must open a list of files whose length is known only at runtime, and every opened file must be closed if a later open fails. Which tool fits?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A retry decorator catches TransientError and re-raises after the last attempt. Why does it use a bare raise instead of raise exc?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A permission check and a result cache are both implemented as decorators. Which order is safer, and why?

βœ…Success

Key Takeaways

  1. A decorator is a callable that replaces a function at definition time. The @ syntax means name = decorator(name), and the wrapper runs on every call.
  2. Always apply functools.wraps to a wrapper, because __name__, __doc__, __annotations__ and the signature are what tracebacks, help and test tools read.
  3. A decorator with arguments has three layers: a factory, a decorator and a wrapper. Using the factory without parentheses binds the wrong object.
  4. Stacked decorators are applied bottom up and entered top down, so the order changes behaviour whenever the layers are not symmetric.
  5. A context manager implements __enter__ and __exit__. Returning a true value from __exit__ suppresses the exception, and that choice should be rare and deliberate.
  6. A generator context manager must wrap its single yield in try/finally, and each instance is single use.
  7. Use a decorator for per-call behaviour and a context manager for per-block cleanup, and use ExitStack when the number of resources is decided at runtime.

References

Every factual claim links to its source, with the access date printed so the source can be rechecked.

  1. Python Software Foundation Β· Kevin D. Smith, Jim J. Jewett, Skip Montanaro, Anthony Baxter Β· Accessed 2026-11-06

  2. Python Software Foundation Β· Accessed 2026-11-06

  3. Python Software Foundation Β· Accessed 2026-11-06

  4. Python Software Foundation Β· Accessed 2026-11-06

  5. Python Software Foundation Β· Guido van Rossum, Phillip J. Eby Β· Accessed 2026-11-06

  6. Python Software Foundation Β· Accessed 2026-11-06

  7. Python Software Foundation Β· Accessed 2026-11-06

  8. Python Software Foundation Β· Accessed 2026-11-06

  9. Python Software Foundation Β· Accessed 2026-11-06

  10. [10]What's New In Python 3.15Documentation

    Python Software Foundation Β· Accessed 2026-11-06

Progress60%