Fix: Python contextmanager Not Working — GeneratorExit, Missing yield, or Cleanup Not Running
Part of: Python Errors
Quick Answer
How to fix Python context manager issues — @contextmanager generator, __enter__ and __exit__, exception handling inside with blocks, async context managers, and common pitfalls.
The Problem
A @contextmanager decorator raises GeneratorExit or doesn’t run cleanup code:
from contextlib import contextmanager
@contextmanager
def open_resource():
resource = acquire_resource()
yield resource
# This never runs if an exception occurs in the with block
release_resource(resource)
with open_resource() as r:
do_work(r)
raise ValueError("Something went wrong")
# resource is never released!Or a class-based context manager raises AttributeError:
class DatabaseConnection:
def __init__(self, url):
self.url = url
def connect(self):
self.conn = create_connection(self.url)
# AttributeError: __enter__
with DatabaseConnection('postgresql://...') as conn:
conn.execute("SELECT 1")Or an async context manager doesn’t work with async with:
# TypeError: 'async_generator' object does not support the asynchronous context manager protocol
async with my_context() as value:
passWhy This Happens
Context managers have strict protocols that must be followed exactly. The with statement compiles to a sequence of method calls — __enter__(), then the body, then __exit__(exc_type, exc_val, exc_tb) — and any deviation from that contract produces the errors above. The @contextmanager decorator wraps a generator function in a small adapter that translates yield into __enter__ (before the yield) and __exit__ (after the yield), but it cannot rescue a generator that doesn’t structure its cleanup correctly.
The most common failure mode is putting cleanup code on the line after yield without a try/finally. When the body of the with block raises, Python calls the generator’s throw() method, which re-raises the exception at the yield point. Without try/finally, that exception propagates up and the cleanup line is never reached. The fix is straightforward but the failure is silent — your resource leaks until something else notices.
A second common failure is treating the class-based protocol as optional. Python does not auto-generate __enter__ from __init__ or any other dunder. A class without __enter__ and __exit__ fails at the with statement itself, before any of your methods run. The AttributeError: __enter__ traceback can be confusing because it does not mention with directly — it just says the attribute is missing.
@contextmanagerdoesn’t handle exceptions by default — if an exception is raised inside thewithblock, it’s thrown into the generator at theyieldpoint. Unless you wrapyieldintry/finally, cleanup code afteryielddoesn’t run on exceptions.- Class-based context managers need
__enter__and__exit__methods — without both, thewithstatement fails withAttributeError. __exit__return value controls exception propagation — returningTruefrom__exit__suppresses the exception. ReturningNone(the default) lets it propagate. This is a common source of unexpected swallowed exceptions.- Async context managers require
__aenter__/__aexit__— regular context managers don’t work withasync with. Use@asynccontextmanagerfromcontextlib.
Fix 1: Use try/finally in @contextmanager
Always wrap yield in try/finally to ensure cleanup runs even when exceptions occur:
from contextlib import contextmanager
# WRONG — cleanup doesn't run on exceptions
@contextmanager
def managed_resource():
resource = acquire()
yield resource
release(resource) # Skipped if exception is raised
# CORRECT — try/finally ensures cleanup
@contextmanager
def managed_resource():
resource = acquire()
try:
yield resource
finally:
release(resource) # Always runs
# Example: managed file with custom logic
@contextmanager
def temp_directory():
import tempfile
import shutil
tmpdir = tempfile.mkdtemp()
try:
yield tmpdir
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
with temp_directory() as tmpdir:
# Work with tmpdir
with open(f"{tmpdir}/file.txt", "w") as f:
f.write("data")
# tmpdir is deleted even if an exception occurs aboveHandling specific exceptions inside the context manager:
@contextmanager
def transaction(db):
"""Context manager that commits on success and rolls back on error."""
tx = db.begin()
try:
yield tx
except Exception:
tx.rollback()
raise # Re-raise — don't swallow the exception
else:
tx.commit() # Only runs if no exception
finally:
tx.close() # Always runsFix 2: Implement Class-Based Context Managers
For complex context managers, use a class with __enter__ and __exit__:
class DatabaseConnection:
def __init__(self, url: str):
self.url = url
self.conn = None
def __enter__(self):
# Called when entering the 'with' block
# Return value is assigned to 'as' variable
self.conn = create_connection(self.url)
return self.conn # or return self
def __exit__(self, exc_type, exc_val, exc_tb):
# Called when leaving the 'with' block
# exc_type, exc_val, exc_tb are None if no exception occurred
if self.conn:
if exc_type is not None:
self.conn.rollback()
else:
self.conn.commit()
self.conn.close()
# Return True to suppress the exception
# Return False (or None) to let it propagate
return False
# Usage
with DatabaseConnection('postgresql://localhost/mydb') as conn:
conn.execute("INSERT INTO users VALUES ('alice')")
# conn.commit() and conn.close() called automatically__exit__ exception parameters:
def __exit__(self, exc_type, exc_val, exc_tb):
# exc_type: the exception class (e.g., ValueError) or None
# exc_val: the exception instance or None
# exc_tb: the traceback object or None
if exc_type is None:
# No exception — normal exit
self.commit()
elif issubclass(exc_type, KeyboardInterrupt):
# Don't suppress Ctrl+C
self.rollback()
return False # Let KeyboardInterrupt propagate
else:
# Other exceptions — rollback and let them propagate
self.rollback()
return False # False = don't suppress
return True # Suppress only if we explicitly decide toFix 3: Reuse Context Managers with contextlib
contextlib provides utilities for building context managers:
from contextlib import contextmanager, suppress, nullcontext, ExitStack
# suppress — silently ignore specific exceptions
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove('temp.txt') # No error if file doesn't exist
# Equivalent to try/except FileNotFoundError: pass
# nullcontext — a no-op context manager for optional context managers
def process(file_path, lock=None):
context = lock if lock is not None else nullcontext()
with context:
with open(file_path) as f:
return f.read()
# ExitStack — combine multiple context managers dynamically
from contextlib import ExitStack
def process_files(file_paths):
with ExitStack() as stack:
files = [
stack.enter_context(open(path)) for path in file_paths
]
# All files open — process them
for f in files:
print(f.read())
# All files closed automatically, even on exception
# ExitStack for conditional context managers
def connect(use_tls: bool):
with ExitStack() as stack:
if use_tls:
stack.enter_context(ssl_context())
conn = stack.enter_context(create_connection())
return connFix 4: Async Context Managers
For async with, use @asynccontextmanager or implement __aenter__/__aexit__:
from contextlib import asynccontextmanager
import asyncio
# @asynccontextmanager — async version of @contextmanager
@asynccontextmanager
async def async_db_connection(url: str):
conn = await create_async_connection(url)
try:
yield conn
finally:
await conn.close()
# Usage
async def main():
async with async_db_connection('postgresql://...') as conn:
result = await conn.fetch("SELECT 1")
# Class-based async context manager
class AsyncCache:
def __init__(self, redis_url: str):
self.redis_url = redis_url
self.client = None
async def __aenter__(self):
import aioredis
self.client = await aioredis.from_url(self.redis_url)
return self.client
async def __aexit__(self, exc_type, exc_val, exc_tb):
if self.client:
await self.client.close()
return False
async def handler():
async with AsyncCache('redis://localhost') as cache:
await cache.set('key', 'value')
value = await cache.get('key')Mixing async and sync context managers in async code:
import asyncio
from contextlib import asynccontextmanager
@asynccontextmanager
async def managed_task(coro):
"""Run a background task and cancel it on exit."""
task = asyncio.create_task(coro)
try:
yield task
finally:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
async def main():
async with managed_task(background_worker()) as task:
await do_main_work()
# Background task cancelled hereFix 5: Context Managers for Resource Management Patterns
Common patterns where context managers shine:
# Temporary working directory
import os
from contextlib import contextmanager
@contextmanager
def working_directory(path: str):
old_dir = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(old_dir)
with working_directory('/tmp'):
os.system('ls') # Runs in /tmp
# Back to original directory
# Timer context manager
import time
from contextlib import contextmanager
from dataclasses import dataclass
@dataclass
class Timer:
elapsed: float = 0.0
@contextmanager
def timer():
t = Timer()
start = time.perf_counter()
try:
yield t
finally:
t.elapsed = time.perf_counter() - start
with timer() as t:
time.sleep(0.1)
heavy_computation()
print(f"Elapsed: {t.elapsed:.3f}s")
# Mocking for tests
from unittest.mock import patch
from contextlib import contextmanager
@contextmanager
def mock_current_time(dt):
with patch('mymodule.datetime') as mock_dt:
mock_dt.now.return_value = dt
yield mock_dt
with mock_current_time(datetime(2026, 1, 1)):
assert get_current_year() == 2026Fix 6: Nesting and Combining Context Managers
Clean syntax for multiple context managers:
# Python 3.10+ — parenthesized with statement
with (
open('input.txt') as src,
open('output.txt', 'w') as dst,
timer() as t,
):
dst.write(src.read())
print(f"Copy took {t.elapsed:.3f}s")
# Python 3.9 and earlier — comma syntax
with open('input.txt') as src, open('output.txt', 'w') as dst:
dst.write(src.read())
# Dynamic number of context managers — use ExitStack
resources = ['db', 'cache', 'queue']
with ExitStack() as stack:
connections = {
name: stack.enter_context(connect(name))
for name in resources
}
process(connections)
# All connections closed automaticallyVersion History: Context Managers Across Python Releases
The with statement landed in Python 2.5 (2006) alongside the contextlib module. The original module shipped with contextmanager (the decorator) and nested (now removed). Class-based context managers — __enter__ and __exit__ — date from the same release. The protocol has been stable for almost two decades, which is why example code from very old Python tutorials still runs.
Python 3.1 (2009) removed contextlib.nested in favor of comma-separated context managers (with A() as a, B() as b:). If you encounter nested(A(), B()) in legacy code, the literal one-line replacement is the comma form. nested had bugs around exception propagation between the two managers, which is why it was removed rather than left as a deprecated alias.
Python 3.2 (2011) added ExitStack, the workhorse for dynamic context manager composition shown in Fix 3 and Fix 6. Before 3.2, combining a runtime-determined number of context managers required a manual try/finally chain that was easy to get wrong, especially when handling exceptions from cleanup. ExitStack.enter_context() solved this and is now the right tool whenever the number of managers is computed.
Python 3.4 (2014) added suppress and redirect_stdout. suppress(SomeError) is the declarative replacement for try: ... except SomeError: pass and reads better in code review. redirect_stdout and the later redirect_stderr (3.5, 2015) are essential for capturing output in tests.
Python 3.7 (2018) introduced asynccontextmanager, shown in Fix 4. Before 3.7, writing an async context manager meant a class with __aenter__ and __aexit__. The decorator made it as easy as the sync version. Python 3.7 also added nullcontext and AbstractAsyncContextManager. The contextvars module (3.7 as well) is a separate feature, but if you use context managers to stash request-scoped state, contextvars is the modern way and supersedes thread-local storage for async code.
Python 3.10 (2021) added parenthesized context managers, shown in Fix 6. Before 3.10, splitting a with line across multiple lines required a backslash continuation:
# Python 3.9 — backslash or single line only
with open('a') as a, \
open('b') as b, \
open('c') as c:
...
# Python 3.10+ — parentheses
with (
open('a') as a,
open('b') as b,
open('c') as c,
):
...The parenthesized form removed an entire category of formatting battles. If you maintain a codebase that targets 3.9 or earlier and you see with (A() as a, B() as b): on a single line, that is the older expression syntax — not the multi-context form — and it works differently. Pin to 3.10+ in pyproject.toml if you adopt the multi-line form.
Python 3.11 (2022) added ExceptionGroup and except*. This matters for context managers that fan out to multiple sub-resources — for example, an ExitStack that catches errors from several __exit__ calls. The __exit__ method is still called sequentially, but if multiple managers raise during cleanup, you can now collect them into a single ExceptionGroup rather than dropping all but the last. The contextlib.chdir() context manager also arrived in 3.11, replacing the hand-rolled version shown in Fix 5.
Python 3.12 (2023) made open() buffered by default for binary mode in a way that matters for some context manager patterns — specifically, the buffer is now flushed on __exit__ even if the inner code did not call flush(). Code that wrote to files and relied on the kernel’s lazy flush behavior may see different timing on 3.12 compared to 3.11.
Python 3.13 (2024) did not change context manager semantics in a way that breaks existing code, but the experimental free-threading build affects __exit__ ordering in concurrent code. If you are testing the no-GIL build, audit any context manager that depends on Python-level locks for ordering.
The practical takeaway: if you can target Python 3.10+, the parenthesized syntax and asynccontextmanager together cover almost all real-world cases. Drop to ExitStack only when the number of managers is genuinely dynamic.
Still Not Working?
@contextmanager generator must yield exactly once — if the generator yields more than once, RuntimeError: generator didn't stop after throw() is raised. The generator must yield exactly one value, then return (or reach the end of the function).
__exit__ called even when __enter__ fails — if __enter__ raises an exception, __exit__ is NOT called. Only wrap __exit__ cleanup for resources acquired in __enter__. For @contextmanager, code before yield is __enter__, after yield is __exit__.
Context managers and generators — if you have a generator function and try to use it as a context manager, it won’t work. Decorate it with @contextmanager to make it work with with.
Returning a value from __exit__ — the return value of __exit__ must be truthy to suppress the exception. return False and return None both let the exception propagate. Only return True when you intentionally want to suppress the exception.
Reusing a @contextmanager instance — a generator-based context manager can be entered only once. Calling the decorated function returns a fresh context manager each time, so cm = my_cm() followed by with cm: ... twice raises RuntimeError: generator didn't yield. Call the factory function each time you need a new context, or convert to a class-based manager whose instances are reusable.
Exceptions raised inside __exit__ shadow the original — if the body raises ValueError and __exit__ then raises OSError during cleanup, the user sees OSError and the ValueError is attached as __context__. Always log both in production code, and consider using ExitStack.push() to handle the chaining manually when a cleanup error must not mask the original failure.
async with on a sync context manager — using async with on a class that defines only __enter__/__exit__ raises AttributeError: __aenter__. Wrap the sync manager in an executor or define an __aenter__ that calls the sync version. For third-party libraries, the contextlib.AbstractContextManager and AbstractAsyncContextManager mixins help structure the wrapper.
For related Python issues, see Fix: Python Async/Sync Mix, Fix: Python Decorator Not Working, Fix: Python asyncio Not Running, and Fix: Python AttributeError NoneType Has No Attribute.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Python Protocol Not Working — Type Checker Rejects Compatible Class, runtime_checkable Fails, or Protocol Not Recognized
How to fix Python Protocol class issues — structural subtyping vs nominal typing, runtime_checkable, Protocol inheritance, TypeVar constraints, and common mypy/pyright errors with Protocol.
Fix: Python pathlib Not Working — Path Object Errors, Joins, and Common Pitfalls
How to fix Python pathlib issues — TypeError with string concatenation, path joining, glob patterns, reading files, cross-platform paths, and migrating from os.path.
Fix: Python asyncio.gather Not Handling Errors — Exceptions Swallowed or All Tasks Cancelled
How to fix asyncio.gather error handling — return_exceptions parameter, partial failures, task cancellation propagation, TaskGroup alternatives, and exception isolation patterns.
Fix: Python Decorator Not Working — Function Signature Lost or Decorator Not Applied
How to fix Python decorator issues — functools.wraps, decorator factories with arguments, class decorators, stacking order, async function decorators, and common pitfalls.