Which return annotation belongs on a @contextlib.contextmanager-decorated function?
answer
- Two objects, inner and outer
- You wrote a generator, not a manager
- The decorator changes the return type
- The type argument is the as-target
- Name it when it travels as a value
basics
~20 sAnnotate the undecorated generator body Iterator[T], where T is the type yielded to the as-target. The decorator turns that function into one returning a context manager, and parameters that accept the manager itself are annotated contextlib.AbstractContextManager[T].
solid answer
~40 s`@contextlib.contextmanager` is applied to a **generator function**, so what you annotate is the generator: a body that yields one connection is `-> Iterator[Conn]` (equivalently `Generator[Conn, None, None]`). The decorator's own declared type maps that to a callable returning a context-manager object, so callers see the right thing without you annotating the decorated result yourself. When you need to *name* that object — a parameter that accepts any `with`-usable manager, or a factory that returns one — use `contextlib.AbstractContextManager[Conn]`, whose type argument is the value bound by `as`. The classic mistake is annotating the decorated function `-> Conn`, the type of the yielded value, which is what the `with` block sees but not what the function returns. The async twin is `contextlib.asynccontextmanager` over an `AsyncIterator[Conn]` body, named by `contextlib.AbstractAsyncContextManager[Conn]`.
code
python · 18 linesimport contextlib
from collections.abc import Iterator
from contextlib import AbstractContextManager
@contextlib.contextmanager
def opened(name: str) -> Iterator[list[str]]:
buffer: list[str] = []
try:
yield buffer
finally:
print(f"closing {name} with {len(buffer)} entries")
def use(cm: AbstractContextManager[list[str]]) -> int:
with cm as buffer:
buffer.append("segment")
return len(buffer)
print(use(opened("memory")))go deeper
Remember what you actually wrote: a generator function that yields once. Its return annotation is Iterator[T], where T is the value the with statement binds after as.
Explain how the decorator changes the type — it maps a callable returning Iterator[T] to one returning a context manager of T — and why you never annotate the decorated signature yourself.
Show where the manager type has to be named: parameters and factories that pass managers as values take contextlib.AbstractContextManager[T], and you should flag the single-use reuse hazard when they do.
Set the convention for resource-handling APIs across a codebase: whether components receive a built manager or a factory that produces one, and how the sync and async families are kept from being mixed.
### Two functions live in one definition Writing `@contextlib.contextmanager` over `def opened(...)` produces two distinct things, and each has its own type. The **inner** object is a generator function. Its contract is: yield exactly once, and the value yielded is what the `with` statement binds to its `as` target. So its return annotation is a generator annotation — `Iterator[Conn]`, or the fuller `Generator[Conn, None, None]` if you prefer to spell out that nothing is sent in and nothing is returned. It is *not* `Conn`, and it is *not* a context-manager type: the function you wrote really does return a generator, and the decorator is what changes that. The **outer** object is the decorated callable. `contextlib.contextmanager` is itself typed to take a callable returning `Iterator[T]` and give back a callable returning a context manager parameterised by the same `T`. That is why annotating the inner generator is enough: type checkers propagate `T` through the decorator, `with opened("memory") as buffer` infers `Conn` for `buffer` automatically, and you never write the decorated signature by hand. ### When you must name the manager type You need the manager's own name whenever a manager travels as a value rather than being used immediately: a parameter that accepts "anything usable in a `with` block yielding a `Conn`", a registry of managers, a factory that picks one of several. That name is `contextlib.AbstractContextManager[Conn]`; `typing.ContextManager` is a deprecated alias of it, kept since Python 3.9 when the `contextlib` spelling became the preferred one. The type argument is the value handed to `as`, which is worth stating explicitly because it is the second common mistake: `AbstractContextManager[Conn]` does not mean "a manager wrapping a `Conn` object" in some vague sense, it means `__enter__` returns a `Conn`. A manager whose `__enter__` returns nothing useful is `AbstractContextManager[None]`. ### It is a real ABC, not only an annotation `contextlib.AbstractContextManager` is an abstract base class with a `__subclasshook__`: any class defining `__enter__` and `__exit__` is recognised as a subclass structurally, so `issubclass(MyManager, contextlib.AbstractContextManager)` is `True` without registration or inheritance. Inheriting from it explicitly buys a default `__enter__` that returns `self` and marks `__exit__` abstract. For a class-based manager, annotate `__enter__` with `typing.Self` when it returns the instance, so subclasses get the subclass type rather than the base. ### The async twin, and one trap `contextlib.asynccontextmanager` decorates an `async def` containing a single `yield`, so the inner annotation is `AsyncIterator[Conn]`, and the resulting object is named `contextlib.AbstractAsyncContextManager[Conn]` and driven by `async with`. The two families are not interchangeable: a synchronous `with` on an async manager fails at runtime because the object implements `__aenter__` and `__aexit__` rather than `__enter__` and `__exit__`, and a checker with the right annotations catches that before it ships. The trap worth remembering is that a decorated `@contextlib.contextmanager` function returns a **single-use** manager: the object wraps one generator instance, so storing it and entering it twice raises at the second `with`. If a shared component in a codebase hands managers around — say a translation-memory updater that stashes one manager on a module-level object so four services reuse it — the annotation says `AbstractContextManager[Conn]` and looks perfectly correct, while the runtime blows up on reuse. Annotate factories as *callables returning* a manager when re-entry matters, rather than passing one built manager around. ### The __exit__ signature, and what the type argument does not cover A class-based manager's `__exit__` takes three parameters — the exception type, the exception instance and the traceback — and returns `bool | None`, where a true value suppresses the exception propagating out of the `with` block. `AbstractContextManager`'s type argument describes only `__enter__`'s result, so it says nothing about suppression; a manager that swallows exceptions and one that never does share the same annotation. If suppression is part of the contract, it belongs in the docstring, and callers should be suspicious of any manager whose `__exit__` returns anything but `None`. ### Managers as values Two stdlib helpers make the manager-as-value case ordinary. `contextlib.ExitStack` enters managers dynamically and unwinds them in reverse, and `contextlib.nullcontext` supplies a do-nothing manager so an optional resource does not force two code paths — both are natural arguments for `AbstractContextManager[T]`-annotated parameters, and `contextlib.AsyncExitStack` plays the same role for the async family. That is the practical reason to know the name at all: as soon as managers are stored, chosen between, or passed to a helper, the annotation has to name the manager rather than the resource. ### Sanity checks before you ship the annotation Ask three questions of any manager signature. What does `as` bind — that is the type argument. Is this `with` or `async with` — that picks `AbstractContextManager` or `AbstractAsyncContextManager`, and the inner generator annotation `Iterator[T]` or `AsyncIterator[T]`. Will the object be entered more than once — if so, hand around a factory rather than a manager built by `@contextlib.contextmanager`, whose object is spent after one use.
- How do you annotate __enter__ on a class-based context manager?With the type the `as` target should receive. When `__enter__` returns the instance itself, annotate `typing.Self` (Python 3.11+) so a subclass's `with` block sees the subclass type rather than the base class. `__exit__` takes the exception type, value and traceback and returns `bool | None`, where a true value suppresses the exception.
- What does inheriting from contextlib.AbstractContextManager actually give you?A default `__enter__` that returns `self` and an abstract `__exit__` you must implement. It is optional: the class defines a `__subclasshook__`, so any class with both `__enter__` and `__exit__` already passes `issubclass` structurally, and `with` never consults the ABC at all — it looks the two methods up on the type directly.
- Why can a manager built by @contextlib.contextmanager not be entered twice?Because the object wraps one generator instance, and that generator is exhausted after the first `with` block finishes; re-entering raises `RuntimeError`. The annotation `AbstractContextManager[T]` cannot express single use, so pass a *callable* that builds a fresh manager whenever a component may enter it more than once.
saying these in an interview costs you the question
- Annotates the decorated function with the yielded value's type
- Thinks the inner generator may yield more than once
- Confuses the manager type with the as-target type
- Uses AbstractContextManager for an async with helper
- Believes typing.ContextManager is a different type
- Reuses one @contextmanager-built manager across several with blocks