Skip to content
Tech Interview Prep home
Technical interview guide

Decorators & Context Managers

Wrapping a function's behavior without changing its code, and guaranteeing setup/teardown runs even when something fails.

Read
47 min
Practice MCQs
25
Interview QA
25
Edition
v2
Editorial status
Reviewed

Scope: Python 3.14 decorator, context manager, contextlib, and asynchronous context manager semantics.

Overview

Curated: · Written: · Reviewed:

Decorators and context managers in Python 3.14

Decorators and context managers solve different lifecycle problems. A decorator replaces a definition and often wraps calls; a context manager brackets an explicit suite with entry and exit behavior. Interview-grade reasoning expands both syntaxes into evaluation order, argument and metadata preservation, exception propagation, suppression, cleanup ownership, and synchronous versus asynchronous execution.

decorator replacement semantics

A decorator receives the defined function or class and its return value is rebound to the original name.

@decorate above def f is approximately f = decorate(f), with decorator expressions evaluated when the definition executes.

Interview trap. Treating a decorator as code that runs only on each call misses import-time registration and replacement.

Engineering practice. Keep decoration-time side effects deliberate, inexpensive, and safe under repeated imports or process startup.

stacked decorator order

Stacked decorators evaluate their expressions top-to-bottom but apply to the function bottom-to-top.

@a then @b on f produces a(b(f)), so wrapper order changes timing, authorization, caching, and exception behavior.

Interview trap. Reading visual order as application order can put logging outside redaction or caching outside tenant authorization.

Engineering practice. Write the equivalent nested call during design and test observable order with focused events.

closure-based wrappers

A function decorator commonly returns a wrapper closure that retains the original callable and configuration.

Each decoration creates closure cells; calls later resolve those retained values even after the decorator function has returned.

Interview trap. Capturing mutable loop variables in decorators can give every wrapper the final loop value through late binding.

Engineering practice. Bind per-wrapper values explicitly and avoid mutable closure state unless synchronization and lifetime are documented.

functools.wraps

functools.wraps copies important metadata and establishes wrapped so introspection can reach the original callable.

It delegates to update_wrapper, preserving names, documentation, annotations, type parameters, and wrapper navigation according to current behavior.

Interview trap. A wrapper can function at runtime yet break documentation, dependency injection, signature inspection, and debugging without metadata.

Engineering practice. Apply @wraps(original) directly to ordinary wrappers and add an explicit signature only when the wrapper truly changes the call contract.

wrapper argument forwarding

A transparent wrapper normally accepts *args and **kwargs and forwards them exactly once to the wrapped callable.

Argument binding still occurs at the eventual function; a wrapper that consumes keywords must separate its own protocol from the target's.

Interview trap. Silently dropping, renaming, or evaluating an argument twice changes behavior under generators, side effects, and keyword-only APIs.

Engineering practice. Preserve the original contract by default and expose intentional interface changes in typing, documentation, and tests.

parameterized decorators

A parameterized decorator adds a factory call: configuration produces a decorator, which then receives the target.

@retry(limit=3) evaluates retry(limit=3) at definition time and applies the returned decorator to the defined function.

Interview trap. Implementing only two layers confuses configuration arguments with the function being decorated.

Engineering practice. Name factory, decorator, and wrapper distinctly; validate configuration eagerly and runtime inputs at call time.

sync and async wrappers

A decorator around an async function must preserve awaitable semantics rather than calling it as though completion were synchronous.

An async wrapper awaits the original coroutine and can use async cleanup; a synchronous wrapper merely returning a coroutine shifts errors and timing.

Interview trap. Timing only coroutine creation reports near-zero duration and misses exceptions raised during execution.

Engineering practice. Detect or separately provide sync and async variants, and preserve cancellation instead of catching broad exceptions.

stateful decorator instances

A callable object can implement a decorator or wrapper with explicit state, but that state may be shared across every call.

call participates like any callable; class decoration and method descriptor behavior require care if the object replaces a function.

Interview trap. An unsynchronized call counter on a singleton wrapper races under threads and mixes tenants or requests.

Engineering practice. Prefer external metrics and request-local state; protect truly shared mutable state and define reset and serialization behavior.

decorating methods

Function wrappers stored on a class normally bind as methods only if the returned object supports descriptor behavior.

Returning a plain function preserves binding; returning an arbitrary callable instance may not automatically inject self.

Interview trap. A decorator that works for module functions can fail with a missing self when applied to methods.

Engineering practice. Return a wrapped function where possible or implement and test get when a custom descriptor is intentional.

class decorators

A class decorator receives the completed class and rebinds its name to the decorator's return value.

It can register, modify, or replace the class without participating in metaclass selection, though interactions with descriptors and dataclasses matter.

Interview trap. Replacing a class with an unrelated callable can break isinstance, subclassing, pickling, and static analysis.

Engineering practice. Prefer returning the same class after explicit registration or use a documented class-generating contract.

decorator exception boundaries

A wrapper should catch only exceptions it can meaningfully handle and preserve traceback and chaining when translating.

finally runs for success and failure; bare except or catching BaseException also intercepts cancellation and process-control exceptions.

Interview trap. A retry decorator that catches every exception can repeat non-idempotent effects or prevent cancellation.

Engineering practice. Use a narrow retry taxonomy, bounded attempts, idempotency analysis, and bare raise for unchanged propagation.

context manager protocol

A synchronous context manager's enter supplies the as-target value and exit always receives completion information once entry succeeds.

exit(type, value, traceback) receives three None values on normal completion and exception details on failure.

Interview trap. Assuming exit receives the context manager or entered resource as its arguments confuses binding with exception reporting.

Engineering practice. Acquire in enter, release in exit, and keep cleanup safe even when the body partially mutates the resource.

with statement translation

The with statement guarantees exit handling around the suite but does not call exit if enter itself fails.

Python obtains the manager methods, calls enter, binds its result, executes the body, then calls exit with normal or exceptional details.

Interview trap. Acquiring resource A and then failing during the same enter before recording cleanup can leak A.

Engineering practice. Use an internal ExitStack when one enter operation acquires several resources that need rollback on partial failure.

exception suppression

exit suppresses an active exception only when it returns a truthy value.

A false value or None continues propagation; normal completion ignores the return value because no exception exists.

Interview trap. Returning self or another accidentally truthy object can hide failures the manager never intended to handle.

Engineering practice. Return True only for a narrow documented exception case and otherwise return False explicitly for clarity.

contextmanager generator

@contextmanager turns a generator function with one yield into a context manager whose pre-yield code enters and post-yield code exits.

An exception from the with body is re-raised at the yield expression, so the generator must re-raise unless it intentionally suppresses.

Interview trap. Catching an exception to log it and then falling off the generator silently suppresses that exception.

Engineering practice. Wrap yield in try/finally for unconditional cleanup and re-raise caught exceptions after logging unless suppression is the contract.

reusable versus single-use managers

A context manager object may be single-use, reusable, or reentrant; those are separate contracts.

Generator-based context manager instances are one-shot, although the decorated factory can create a fresh manager for each with statement.

Interview trap. Saving one generated manager and entering it twice causes protocol errors rather than reacquiring cleanly.

Engineering practice. Expose a factory when each operation needs fresh state and document reentrancy for lock-like managers.

contextlib.suppress

contextlib.suppress ignores only the specified exception types raised in its body.

It is equivalent to a very narrow try/except/pass and supports documented handling for exception groups in modern Python.

Interview trap. Suppressing broad Exception around a large suite can erase programming bugs and leave partially completed work.

Engineering practice. Keep the body tiny, list expected exception types, and use explicit handling when recovery or observability is required.

closing and aclosing

closing calls close on exit and aclosing awaits aclose, adapting resources that lack native context-manager support.

aclosing is especially useful for deterministic async-generator cleanup in the same task and context as iteration.

Interview trap. Wrapping an already context-managed resource in closing can call the wrong lifecycle method or duplicate cleanup.

Engineering practice. Prefer the resource's native with or async-with protocol and use adapters only when its documented API requires them.

nullcontext

nullcontext provides an optional no-op context manager and returns a supplied enter result.

It lets one with statement cover caller-owned and locally-created resources without duplicating the body.

Interview trap. Using nullcontext does not transfer ownership; caller-owned resources must not be closed by the branch.

Engineering practice. Make ownership explicit in names and documentation, then select the real manager or nullcontext at the boundary.

ExitStack dynamic cleanup

ExitStack manages a last-in-first-out stack of exit callbacks and context managers chosen dynamically.

enter_context registers an entered manager's exit, callback registers cleanup, pop_all transfers the remaining cleanup responsibility.

Interview trap. Calling callback with a function that needs exception details is not equivalent to pushing an exit-style callback.

Engineering practice. Use ExitStack for variable resource counts, partial acquisition rollback, and all-or-nothing setup with explicit ownership transfer.

ExitStack exception flow

ExitStack unwinds callbacks in reverse order and lets inner exits suppress or replace exceptions before outer exits observe them.

This mirrors nested with statements, including updated exception information as each exit function runs.

Interview trap. Assuming every callback sees the original exception can make logging and transaction decisions inconsistent.

Engineering practice. Keep each cleanup independent and idempotent, and test failures during both body execution and earlier cleanup.

AsyncExitStack

AsyncExitStack combines asynchronous and synchronous cleanup for dynamically acquired resources.

It can enter async context managers, register coroutine cleanup functions, and unwind them in reverse order when awaited.

Interview trap. Forgetting to await aclose or leave async with means cleanup coroutines may never execute.

Engineering practice. Bind stack lifetime to async with and preserve cancellation while giving critical cleanup a deliberate strategy.

transaction context managers

A transaction manager should commit only on successful completion and roll back on failure without hiding the original exception.

exit can inspect exception state, perform rollback, and return false so propagation continues; cleanup failures may require exception chaining.

Interview trap. Committing in finally commits failed work, while returning true after rollback can falsely report success.

Engineering practice. Define nested transaction and savepoint behavior, make rollback robust, and preserve evidence when rollback itself fails.

locks as context managers

Using a lock as a context manager pairs acquisition and release across returns and exceptions.

Entry acquires and exit releases, but the protected region still needs minimal scope and a documented ordering among multiple locks.

Interview trap. A with statement prevents a forgotten release but cannot prevent deadlock, blocking I/O under lock, or logical races.

Engineering practice. Keep critical sections small, establish lock order, and avoid invoking unknown callbacks while holding a lock.

choosing decorators and managers

Decorators transform definitions or calls, while context managers delimit a resource or state lifetime around a visible block.

Both centralize cross-cutting behavior, but decorators can hide per-call work whereas with statements expose scope and ownership at the call site.

Interview trap. Using a decorator for a resource whose lifetime should cover only part of a function holds it too long and obscures boundaries.

Engineering practice. Choose the construct that makes timing, ownership, failure, suppression, and async behavior easiest to review.

Worked example: stacked decorators apply bottom-to-top

@authorize then @cache on f is authorize(cache(f)). The cache sees the raw function; authorize sees the cached wrapper. Reverse the stack and you cache unauthorized results.

stack written top-to-bottomequivalent callwho wraps f first
@authorize then @cacheauthorize(cache(f))cache
@cache then @authorizecache(authorize(f))authorize

A with around only the body that needs the lock is the same interview from the other side: lifetime is the suite, not the definition.