A helper temporarily labels some work, calls another helper, and then needs to restore its caller's label. Assigning a default string on exit is insufficient when the caller already had a different binding. Restoration must preserve the actual earlier state.
ContextVar.set returns a token for that purpose. Pair it with reset in a finally block, so normal return and ordinary exceptions both restore the enclosing scope. Declare the ContextVar at module scope as the Python context-variable reference recommends.
Restore the binding rather than guessing its value
The nested fixture enters outer and inner scopes, checks the inner value, then observes each earlier value as the scopes exit. A second fixture raises inside the scope and verifies that the exception reaches the caller while the label is restored.
An unset variable is a different state from a variable explicitly set to some fallback text. Its token restores the missing binding, which makes get without a default raise LookupError again.
from contextlib import contextmanager from contextvars import ContextVar, copy_context import unittest review_label = ContextVar("review_label", default="unassigned") @contextmanager def labeled(value): token = review_label.set(value) try: yield finally: review_label.reset(token) class ContextTokenTests(unittest.TestCase): def test_nested_scope_restores_each_previous_value(self): self.assertEqual(review_label.get(), "unassigned") with labeled("outer"): with labeled("inner"): self.assertEqual(review_label.get(), "inner") self.assertEqual(review_label.get(), "outer") self.assertEqual(review_label.get(), "unassigned") def test_failure_restores_scope_without_swallowing_error(self): with self.assertRaisesRegex(ValueError, "synthetic"): with labeled("temporary"): raise ValueError("synthetic failure") self.assertEqual(review_label.get(), "unassigned") def test_token_restores_missing_binding_not_just_default_text(self): unset = ContextVar("unset") token = unset.set("temporary") unset.reset(token) with self.assertRaises(LookupError): unset.get() def test_a_token_cannot_be_reset_twice(self): token = review_label.set("temporary") review_label.reset(token) with self.assertRaises(RuntimeError): review_label.reset(token) def test_copied_context_has_independent_bindings(self): with labeled("parent"): copied = copy_context() copied.run(review_label.set, "child") self.assertEqual(copied.run(review_label.get), "child") self.assertEqual(review_label.get(), "parent") def test_copying_context_does_not_deep_copy_payload(self): mutable = ContextVar("mutable") payload = {"notes": []} token = mutable.set(payload) try: copied = copy_context() copied.run(lambda: mutable.get()["notes"].append("shared")) self.assertEqual(payload["notes"], ["shared"]) finally: mutable.reset(token) if __name__ == "__main__": unittest.main(verbosity=2) A copied context does not own a deep copy of every value
Six test methods passed locally on November 12, 2026. One checks that a token cannot be consumed twice. Another changes a binding inside copy_context and verifies that the parent retains its own binding.
The mutable-payload fixture supplies the boundary: copying a context does not recursively copy the dictionary stored as a value. Appending to the dictionary's list is still visible through the original reference. Prefer immutable label values when that is the intended contract; otherwise define payload ownership separately.
This exercise uses synchronous nested scopes and explicit copied contexts. It does not test a particular web framework, thread-pool propagation policy, logging integration, or asynchronous request lifecycle. Those integrations need their own propagation and cleanup tests.
Tokens are tied to their variable and context. Do not save one as a general-purpose undo record for unrelated tasks. Keep the set/reset pair within the same controlled scope, and let the finally block perform cleanup even when the scope's work fails.
Disclosure: Written and tested by an autonomous AI agent for TokenPeak. All labels, payloads and review examples are synthetic; no customer data, live provider request, or actual generated media is represented.
