Scope
Local scope: variables inside a function stay inside it
A variable created inside a function doesn’t exist outside it:
temperature only exists while set_temperature() is running, in its own
local scope. This is true even if a variable with the same name exists
outside the function — they’re unrelated:
Reading an outer variable works — reassigning it doesn't, without global
A function can read a variable from outside it, as long as it doesn’t try to assign to it:
But trying to reassign it inside the function, without saying so explicitly, doesn’t do what you’d expect:
The moment Python sees an assignment to call_count anywhere in the
function, it treats call_count as local to that function for its entire
body — including the read on the right-hand side, before the local one has
been created. The global keyword tells Python explicitly which one you
mean:
Why relying on global is usually a bad idea in agent code specifically
global works, but it makes a function’s behavior depend on state the
function’s signature doesn’t reveal — you can’t tell what track_call()
actually depends on just by looking at how it’s called. In agent code, this
gets worse fast: if multiple parts of an agent loop mutate the same global
variable (say, a shared conversation history or token count), it becomes
very hard to reason about what state looks like at any given point,
especially once things run concurrently. The far more common and safer
pattern is to pass state in and return it out explicitly:
Same result, but track_call’s dependency on call_count is now visible
in its signature, not hidden behind a global statement elsewhere.
The mutable default argument trap
One specific scope-related gotcha worth knowing before it bites you: a
default argument is evaluated once, when the function is defined — not
once per call. This is harmless for immutable defaults like 0 or
"hello", but dangerous for mutable ones like a list:
The second call’s output likely surprises you: tools=[] isn’t a fresh
empty list each time, it’s the same list object, created once when the
function was defined, silently accumulating across every call that doesn’t
pass its own. The standard fix is to default to None and create the list
inside the function body instead:
A third scope: closures
A function defined inside another function can “remember” variables from the outer function, even after the outer function has finished running:
multiply is a closure: it “closes over” factor from
make_multiplier’s scope. Each call to make_multiplier creates a
separate factor, which is why double and triple don’t interfere with
each other, even though both came from the same function.
This is exactly the shape of a tool factory — a function that returns a configured tool function without needing a class:
nonlocal — the closure equivalent of global
Same problem as before: reading an enclosing variable works, but
reassigning it needs an explicit keyword — nonlocal instead of global,
since this is the enclosing function’s scope, not the module-level global
scope. Two separate counters, run a different number of times, make it
clear each one keeps its own independent state:
counter_a and counter_b each came from their own call to
make_counter(), so each has its own separate count — running one three
times and the other five times doesn’t affect the other’s total at all.
Without nonlocal, count += 1 would raise the same UnboundLocalError
you saw earlier with global,
for the same reason: Python would treat
count as a new local variable the moment it sees an assignment to it.
How this relates to object-oriented code
If you’re used to reaching for a class whenever you need a “thing” that
remembers state between method calls, a closure is doing the same job in
miniature. Here’s make_counter rewritten as a class, for comparison:
Line up the two versions: self.count in the class plays the same role as
count remembered by the closure — both are state that persists across
calls, private to one instance. increment(self) plays the same role as
the inner increment() function — both are the one operation you can
perform on that state. A closure is essentially a lightweight object with
exactly one method and no name for its state — useful when you need
“some remembered state plus one operation” and a full class would be more
ceremony than the problem needs. Once you need multiple related
operations sharing that state, a class (covered next lesson) is the better
fit.
After a function that creates a local variable finishes running, can that variable be accessed outside the function?
Handle the case where registry is None by creating a fresh list inside the function, then add name only if it isn't already present, and return the registry.
Define an inner function that takes a number and checks it against min_value/max_value from the enclosing scope, then return that inner function.