Post-"with" binding of a context manager result

Some context managers are designed to produce a value on exit rather than provide one on entry, for example a stopwatch that measures duration, or a builder that yields a constructed object. The current protocol has no way to express this, leaving authors with an awkward choice:

# The as-binding happens at __enter__, so sw is accessible inside the wrapped block
with stopwatch() as sw:
    do_work()
    sw.duration  # but duration isn't ready yet

The usual workarounds are a result-container (sw.duration starts as None and is set on exit) or a runtime guard that raises if accessed early. Both work, but neither lets a type checker enforce the constraint statically.

What do you think about introducing a typing-only convention (so no runtime or grammar changes) where a context manager can opt into “post-exit binding” semantics by annotating __enter__ as returning a special sentinel type (e.g. typing.Deferred[T]). Type checkers would then:

  1. Treat the as-bound variable as Deferred[T] (effectively unusable) inside the with block body.
  2. Narrow it to T in the scope after the block completes.

IIUC this maps somewhat naturally onto existing control-flow narrowing - it would be the same mechanism used after an assert or isinstance check, just triggered by block exit rather than a boolean predicate.

2 Likes

I don’t really see an advantage to this over being forced to add assert sw.duration is not None or calling .get on a result container.

Such a construct would be unsound, since there’s no way for the type checker to validate that the return value from __enter__ is behaving as promised. We do have other unsound annotations, like overload or TypeGuard, but I think the bar for introducing new unsound annotations should be that it would help describe a lot of current real world Python and that there is no easy sound alternative.

1 Like

I’m not sold this should be done via the retval of __enter__. But marking post closure attrs would be nice to have, for sure.

How would it work with reusable, and with reentrant CMs? Should they just not opt-in?