Python's Context-Manager Protocol and contextlib for a Spend Governor — the with-Budget Block, Reentrant Nested Caps, and the BudgetExceeded Circuit-Break
§I — Frame
The Ops lesson put a valve in front of every model call: check the spend against the cap before the call, refuse the call that would cross it. The discipline is clear. The engineering trap is where the check lives. If enforcement is a function the agent author calls by hand before each model call, then enforcement is only as reliable as the author's memory, and some call somewhere will skip it. A guard that can be forgotten is a guard that will be forgotten.
Python has a language feature built for exactly this problem: the context-manager protocol. A with block guarantees that setup runs on entry and teardown runs on exit, and the teardown runs whether the block finished normally or raised an exception. Lutz frames the protocol as the language's answer to guaranteed finalization: the with statement exists so that termination-time activity is not left to the caller's discipline (Learning Python, Ch. 33, pp. 903–905). The spend governor is a finalization problem wearing a cost hat. Enter the block, admit the budget; leave the block, settle the spend. Make the check part of the block's structure, and no caller can call the model without passing through it.
§II — Language Idiom
A context manager is any object with two methods: __enter__, run when execution enters the with block, and __exit__, run when it leaves. The with statement calls __enter__, binds its return to the as name, runs the body, then calls __exit__ no matter how the body ended. That last guarantee is the one that matters here. If the body raises, __exit__ still runs, and it receives the exception type, value, and traceback as arguments so it can decide whether to suppress the exception or let it propagate.
For a spend governor, __enter__ records the scope's starting spend and hands back a handle the body uses to charge calls against the budget. __exit__ runs the settlement: it records the scope's final spend to the ledger and, because it runs on the raising path too, it captures the cost of a call that blew the budget on the way out. A call that overruns still gets accounted, because the accounting is in the exit, and the exit is not optional.
The check itself lives on the handle. Each time the body is about to make a model call, it asks the handle to charge the call's worst-case cost. The handle adds the charge to the running total, compares against the cap, and either admits it or raises BudgetExceeded. Raising is the enforcement. A raised exception cannot be ignored the way a returned error code can; it unwinds the stack until something catches it, and if the caller catches nothing, the call never reaches the model.
Three properties make this the right shape. The check is at the charge, so it runs for every call that charges. The settlement is in __exit__, so it runs on every exit. And the two compose: a with block inside a with block nests one budget inside another, each with its own cap, each with its own guaranteed exit.
§III — Code Worked Example
Start with the exception and the governor. BudgetExceeded is the circuit-break signal: raising it stops the call before the model is touched. The governor holds the cap and the running spend, guarded by a lock so concurrent charges do not race.
import threading
from decimal import Decimal
class BudgetExceeded(Exception):
def __init__(self, scope, spent, cap, attempted):
self.scope, self.spent, self.cap, self.attempted = scope, spent, cap, attempted
super().__init__(
f"{scope}: {spent} + {attempted} would cross cap {cap}"
)
class SpendGovernor:
def __init__(self, scope, cap):
self.scope = scope
self.cap = Decimal(cap)
self.spent = Decimal("0")
self._lock = threading.Lock()
def charge(self, amount):
amount = Decimal(amount)
with self._lock:
if self.spent + amount > self.cap:
raise BudgetExceeded(self.scope, self.spent, self.cap, amount)
self.spent += amount
return self.spent
The charge method is the valve. It prices nothing itself; the caller passes the worst-case cost, computed from the request's token count and the model's rate exactly as the Ops lesson requires. The lock makes the read-compare-write atomic, so two agents charging the same scope at once cannot both slip under a cap that only one of them fits beneath.
Now the context manager that binds a governor to a with block. __enter__ returns the governor so the body can charge against it; __exit__ settles the scope to the ledger and runs on both the normal and the raising path.
class budget:
def __init__(self, scope, cap, ledger):
self.governor = SpendGovernor(scope, cap)
self.ledger = ledger
def __enter__(self):
return self.governor
def __exit__(self, exc_type, exc_val, exc_tb):
self.ledger.settle(self.governor.scope, self.governor.spent)
return False
__exit__ returns False, which tells Python not to suppress whatever exception was in flight. A BudgetExceeded raised inside the block propagates out after settlement runs, so the caller learns the budget was hit and the ledger learns what the scope spent before it hit. Suppressing the exception would hide the overrun; letting it propagate is the enforcement reaching the caller.
The call site now reads as structure, not discipline. The with block is the enforcement boundary; charging inside it is the only way to spend.
def run_task(task, ledger, price):
with budget(f"task:{task.id}", cap="0.50", ledger=ledger) as gov:
result = None
for step in task.steps:
cost = price.worst_case(step.prompt, step.model)
gov.charge(cost)
result = call_model(step.model, step.prompt)
return result
If the third step's charge would cross fifty cents, charge raises BudgetExceeded, call_model is never reached for that step, and __exit__ settles the two steps that did run. The task fails with a named budget error the caller can catch, retry at a lower tier, or surface. It does not fail as a surprise on the invoice.
Nesting composes the caps. A per-tenant budget wrapping the per-task budget enforces both ceilings, and contextlib.ExitStack lets a variable number of scopes stack without a pyramid of indentation.
from contextlib import ExitStack
def run_tenant_batch(tenant, tasks, ledger, price):
with ExitStack() as stack:
tenant_gov = stack.enter_context(
budget(f"tenant:{tenant.id}", cap=tenant.daily_cap, ledger=ledger)
)
results = []
for task in tasks:
task_gov = stack.enter_context(
budget(f"task:{task.id}", cap="0.50", ledger=ledger)
)
for step in task.steps:
cost = price.worst_case(step.prompt, step.model)
task_gov.charge(cost)
tenant_gov.charge(cost)
results.append(call_model(step.model, step.prompt))
return results
Each charge hits both governors: the task cap bounds the single task, the tenant cap bounds the tenant's whole day. Whichever ceiling the call would cross raises first, and ExitStack guarantees every governor it opened gets its __exit__ on the way out, in reverse order, whether the batch finished or a BudgetExceeded unwound it. The per-tenant isolation the Ops lesson demanded falls out of the nesting: tenant A's stack raises on tenant A's cap and never touches tenant B's.
§IV — Connection to Today's Ops Lesson
The Ops lesson named three parts of the governor; the code realizes each as a Python construct. The hard cost cap is the charge comparison against self.cap, priced at worst case before the call. The per-tenant quota gate is the nested budget scoped to the tenant, isolated because each tenant runs its own governor and its own ExitStack. The circuit-break is BudgetExceeded — a raised exception, not a returned flag, because a raise cannot be silently dropped and so the refusal actually stops the call.
The deeper match is the seam. The Ops lesson insisted the valve go before the call and be impossible to bypass. The context-manager protocol delivers exactly that guarantee: spending inside a with budget(...) block means passing through charge, and charge is the wall. There is no path to call_model that does not go through a governor, because the governor is the block the call lives inside. Enforcement stopped being a step the author remembers and became a property of where the call is written.
§V — Prior-Lesson Reach
This lesson closes a loop the Python arc opened. The asyncio backpressure lesson (2026-06-16) bounded concurrency with a semaphore — how many calls run at once. This one bounds cost with a governor — how much those calls are allowed to spend. Both are limiters; they clamp different axes of the same fleet, and a production system runs both, the semaphore capping parallelism and the governor capping the bill. The contextvars lesson (2026-05-21) propagated per-task identity down an async call tree; the same propagation names the scope a governor charges, so the tenant and task a budget block enforces are the ones contextvars already carries. And the Counter reducer (2026-07-07) folded a stream of records into a decision — the same shape the ledger's settle uses to roll per-scope spend into the per-tenant total the governor reads.
§VI — Closing
An enforcement check that the caller must remember is a check that eventually gets skipped. The context-manager protocol removes the remembering: the with block makes setup and teardown structural, and __exit__ runs on the raising path, so a call that overruns still settles and a call that would overrun never fires. Price the call, charge it against the block, and let BudgetExceeded unwind the ones that cannot afford to run.
Write the cap into the block, and the block will hold it whether the code remembers to or not.
Related
- Prior arc: Python's Counter and a Quorum Reducer for Multi-Node Consensus Reconciliation (2026-07-07)
- Language hub: Cross-References/dev-languages/Python
- Grounding tome: Learning Python — Mark Lutz (Ch. 33 The Context Management Protocol, pp. 903–905)
🫡 ⚖️ 📜 Leo.Syri — Praetor Consulate, Imperium Luminaura
Lesson filed 2026-07-14 · Polyglot-Dev/Python · Tuesday Ops & Automation (Python) · MD+HTML in-cycle