How do you write a context that manages global state properly? I tried:
import contextlib
_foo_context = None
def get_foo()->str|None:
return _foo_context
@contextlib.contextmanager
def with_foo(new_context:str):
global _foo_context
_foo_context, old_context = new_context, _foo_context
yield
_foo_context = old_context
But that fails when used with generators(and async):
def cook_food(item:str):
with with_foo(item):
print(f"Cooking {get_foo()}")
yield
print(f"Still cooking {get_foo()}")
raw_eggs = cook_food("eggs")
raw_spam = cook_food("spam")
next(raw_eggs,None)
next(raw_spam,None)
next(raw_eggs,None) # prints "Still cooking spam", instead of eggs.
next(raw_spam,None) # prints "Still cooking None"!
How do I do this properly?
MRAB
(Matthew Barnett)
October 18, 2025, 7:42pm
2
Do you know why it’s doing that?
When it calls the first next(raw_eggs,None), it resumes cook_food with “eggs”, setting _foo_context to “eggs”, and then yields.
When it calls the first next(raw_spam,None), it resumes cook_food with “spam”, setting _foo_context to “spam”, and then yields.
When it calls the second next(raw_eggs,None), it resumes the first cook_food, printing “spam” because _foo_context is still set to “spam”.
You can’t have 2 context managers sharing a global variable, yet storing 2 separate values in it, at the same time.
2 Likes
trendels
(Stanis Trendelenburg)
October 20, 2025, 4:37pm
3
There is a way to manage global state that works with async and threads, and that is to use contextvars instead of global variables. For example:
import asyncio
import contextlib
from contextvars import ContextVar
_foo_context = ContextVar("_foo_context", default=None)
def get_foo()->str|None:
return _foo_context.get()
@contextlib.contextmanager
def with_foo(new_context:str):
token = _foo_context.set(new_context)
try:
yield
finally:
_foo_context.reset(token)
async def cook_food(item:str):
with with_foo(item):
print(f"Cooking {get_foo()}")
await asyncio.sleep(0.1)
print(f"Still cooking {get_foo()}")
async def main():
tasks = [
asyncio.create_task(cook_food("eggs")),
asyncio.create_task(cook_food("spam")),
]
await asyncio.wait(tasks)
if __name__ == "__main__":
asyncio.run(main())
This prints
Cooking eggs
Cooking spam
Still cooking eggs
Still cooking spam
Unfortunately, it does seem contextvars do not currently work with generators. Changing your original example to use a contextvar instead of a global variable does not change the result.
This was surprising to me, TBH. Doing a quick google search I found that there is already PEP about this. It seems this is a know problem but there is (was) no obvious solution available when contextvars were introduced in Python 3.7:
Context variables provide a generic mechanism for tracking dynamic, context-local state, similar to thread-local storage but generalized to cope work with other kinds of thread-like contexts, such as asyncio Tasks. PEP 550 proposed a mechanism for...