Skip to content
Preptima
mediumCodingConceptMidSenior

A context manager written with @contextmanager leaves a lock held whenever the with body raises. What is missing from the generator?

A try/finally around the yield. @contextmanager throws the body's exception into the generator at the yield, so a release written after the yield is skipped. Wrap the yield in try/finally, or use with lock inside it, and re-raise anything you catch, or the generator suppresses the caller's error.

4 min readUpdated 2026-10-09Target archetype: Product Startup

Answer

Short answer

The generator has no try/finally around its yield. A @contextmanager generator receives the with body's exception at the yield itself, so the release() written on the next line never runs and the lock stays held. Wrap the yield in try/finally, or simply write with lock: yield, and re-raise anything you catch.

@contextmanager
def held(lock):
    lock.acquire()      # outside the try: a failed acquire has nothing to release
    try:
        yield
    finally:
        lock.release()  # runs on success, on an exception, and on GeneratorExit

Was this answer helpful?

Likely follow-ups

  • What does @contextmanager raise if the generator yields a second time after the body has failed?
  • Why does an except Exception clause around the yield still let asyncio cancellation through, and is that what you want?
  • How would you hand the locks acquired inside an ExitStack over to a caller who releases them later?
  • What changes when the same helper is written with @asynccontextmanager around an asyncio.Lock?

Related questions

Further reading

Related Topics