context parameters¶
a context parameter is filled implicitly at call sites from the context
declarations in scope, threading ambient values (a config, a logger, a
connection) through call chains without explicit forwarding
def f(a: int, context b: str): ...
f(1) # error: no context value found for parameter `b`
context s1 = "asdf"
f(2) # ok — `s1` is passed implicitly
context s2 = "fdsa"
f(3) # error: ambiguous — `s1` and `s2` both match
surface syntax¶
context marks a parameter, written ahead of its name:
and declares a value, written ahead of an assignment. the declaration may carry an annotation; without one the variable is typed by its value:
context is a prefix on a declaration rather than a form of its own, so it
composes with the rest of the declaration modifiers in either
order. the other keywords decide what the declaration is — let makes it
Final, private hides it — and context only adds the candidacy:
it is not a modifier on a def or a class, which declare no variable for a
call site to read
resolution¶
resolution is by assignability, not by name: a declaration is a candidate for a parameter when its type is assignable to the parameter's declared type. the rules:
- the innermost scope with at least one candidate wins; a declaration in the calling function shadows a module-level one
- two candidates in the winning scope are an error — the call must pass the argument explicitly
- in the scope containing the call, only declarations lexically before the call count. enclosing-scope declarations count regardless of position, matching how closed-over names are read late
- a declaration a nearer scope shadows is not a candidate. the call site passes the resolved name, so a scope in between holding that name would have the call read its value instead
- an explicit argument (positional or keyword) always wins; no resolution happens for a parameter that is matched
a function's own context parameters are declarations in its body scope, so a
requirement propagates through call chains:
def f(context b: str) -> str:
return b
def g(x: int, context b: str) -> str:
return f() # ok — g's own `b` fills it
trailing lambda blocks¶
a trailing lambda block binds the value its callback is
called with as it, and a block bound to an
implicit receiver spells that receiver self. nobody
writes either binding, so both are ambient in the block body the way a context
declaration is ambient in its scope — and both fill context parameters:
def log(message: str, context level: int): ...
def at(level: int, fn: (int) -> None): ...
at(2):
log("started") # ok — the block's `it` fills `level`
only the innermost block's implicit names count. every block binds it, so a
nested block always shadows the one around it, and reaching past it would name a
value the call never receives. a callback both a receiver and an it fit is
ambiguous, like any two candidates in one scope
a block whose callback shape cannot be inspected — an unannotated callee
parameter, or one with no argument for the block to bind — leaves it untyped,
and an untyped it offers nothing rather than fitting every parameter
parameter placement¶
a context parameter receives its implicit argument by keyword, so it must
not sit where explicit positional arguments could land on it. positional
parameters after a context parameter, positional-only context parameters,
and *args after a context parameter are parse errors. keyword-only
context parameters (after *) are unrestricted
lowering¶
the emitted python is ordinary code: prefixes are stripped and every call site passes its resolved argument explicitly, by keyword
the lowering is intentionally lossy — there is no marker in the output and no reverse transform. a round-trip degrades context calls to the explicit form, which is also valid basedpython. python callers of transpiled code simply pass the argument themselves
diagnostics¶
missing-context-argument— acontextparameter is unmatched and nothing in scope fits itambiguous-context-argument— several declarations in the winning scope fit it
limitations¶
- resolution happens only at direct call expressions (
f(...)) whose callee is a plain function or bound method with a single signature. constructors,__call__instances, overloaded functions, union-typed callees, dunder dispatch, and calls with*/**unpacking all keep the plain missing-argument behaviour — pass the argument explicitly there - candidates are typed at their declaration site: reassigning a context variable to a different (assignable) type between declaration and call is not tracked
- a declaration inside a conditional branch counts lexically; whether the branch executed is not tracked
inlay hints¶
a call site shows the arguments it fills implicitly, written where the lowering writes them — after the explicit arguments, by keyword. each variable navigates to the declaration that resolved it:
def f(context a: int) -> None: ...
def g(x: str, context a: int) -> None: ...
context b = 1
f(⟨a=b⟩)
g("y"⟨, a=b⟩)
a parameter given explicitly is not hinted, and neither is one that failed to
resolve — missing-context-argument and ambiguous-context-argument already
say so