Scope, Defaults, and Extra Arguments
Local vs global names, default arguments, *args and **kwargs, and why a list default leaks agent memory.
Scope means “where a name lives.” A local name lives inside one function. It is born when the function starts. It dies when the function ends. Code outside cannot read it. A second call does not remember the previous local names — unless you cheated with a default list, which this lesson forbids.
A global name lives in the whole file. Functions can read it. Changing it from inside a function is easy to get wrong. Prefer passing values in and returning values out. Agents should pass state in and get state back.
Local names die when the function ends. Do not hide agent memory in a global or a list default.
Pass in, return outThis lesson also covers defaults (inputs you may skip) and extra inputs. Together, these rules decide whether two users share memory by accident.
What you will learn
- Local names vs global names
- Default arguments
- Why a list default is a bug
argsand*kwargsas extra inputs
Local vs global
Assignment inside a function makes a local name. The name outside does not change.
score = 10
def bump():
score = 1
print(class="tok-s">"inside, score is", score)
bump()
print(class="tok-s">"outside, score is still", score)The score = 1 line makes a new local score. The global score stays 10. If you only read a global, Python uses it. If you assign, Python treats the name as local for the whole function. Then print(score) before the assignment inside the function can raise UnboundLocalError. If you need to read and then store a new value, pass the old value in.
If you need a new value, return it:
score = 10
def next_score(current):
return current + 1
score = next_score(score)
print(score)That is safer than editing a global. global score exists. Do not use it in agent tools. Hidden writes make tests lie.
Reading a constant like ALLOWED = {"search", "read"} from inside a function is normal. You are not assigning. You are looking up a policy.
Default arguments
A default is a value used when the caller skips that input.
def search(q, k=3): lets you call search("rain"). Then k is 3. You can still pass k=1.
Defaults are set once, when Python first creates the function. They are not rebuilt on every call. That sentence is the whole list-default bug.
Parameters with defaults must sit after parameters without defaults. def search(q="rain", k): is illegal. def search(q, k=3): is legal.
Never use a list as a default
This looks handy. It is a bug:
def add_event(event, history=[]): class="tok-c"># BAD
history.append(event)
return historyEvery call that skips history shares one list. The second call still sees the first event. In an agent, that looks like memory leaking from one user to the next. The same bug happens with dict defaults: def f(x, cache={}):.
Fix: default to None. Make a new list inside the function.
| Default | Safe? | Why |
|---|---|---|
k=3 | Yes | Numbers cannot change in place |
label="user" | Yes | Strings cannot change in place |
history=[] | No | One list is reused; append keeps old items |
history=None then [] | Yes | Each call can get a fresh list |
Never use a list or a dict as a default. Use None, then build a new one inside. If the caller passes a list, use that list. If they skip it, make a new one. That is how a tool can optionally record into a transcript you own.
Extra inputs: *args and **kwargs
Sometimes you want a function to accept extra values you did not name one by one.
*argsmeans extra positional inputs (in order). It is a tuple — a fixed row of values.kwargsmeans extra named inputs. It is a dict** — a map of names to values.
Say it in plain words: extra inputs. The names args and kwargs are convention. parts and *fields work too. The stars are the syntax.
| Form | Where | Meaning |
|---|---|---|
*args | in def | Extra values in order, as a tuple |
**kwargs | in def | Extra named values, as a dict |
*row | in a call | Unpack a list or tuple into order |
**d | in a call | Unpack a dict into names |
def log_all(*parts, **fields):
print(class="tok-s">"parts", parts)
print(class="tok-s">"fields", fields)
log_all(class="tok-s">"tool", class="tok-s">"search", ok=True, ms=12)A tool runner often looks like def call_tool(name, kwargs): then fn(kwargs). Extra named inputs pass through. Required names stay explicit: name is not in kwargs. That split is useful. The tool name is policy. The rest is payload.
Do not swallow extras forever without noticing. Unknown keys on a strict tool should error. Unknown keys on a wrapper may pass through. Choose.
Common mistakes
- List or dict defaults.
- Assigning to a name inside a function and thinking the global changed.
globalto dodge passing state.- Calling
fn(kwargs)instead offn(**kwargs)— you pass one dict as the first argument. - Putting
argsafter*kwargsin the definition (illegal order).
Run to execute this in your browser. Nothing is sent to a server.
You should see bad ['x'] then bad ['x', 'y']. That shared list is the leak. The good function prints ['a'] then ['b'].
How agents use this
call_tool(name, **kwargs) plus a registry is the usual runner. Defaults give a search tool a sensible k=5 when the model skips a field. A list default will merge two chats into one. Pass the transcript in. Default it to None. Make a new list for each agent.
Global registries of tools are fine as constants. Global transcripts are not. If MEMORY = [] sits at module top and every request appends, users share a diary. That is the list-default bug at file scale. Return new state. Or pass a per-run list.
When the model omits k, the default saves you. When the model sends an extra key, **kwargs on a strict search(q, k=3) still raises unless you filter the dict first. Filtering unknown keys at the executor is a good guard. Passing them blindly is how a renamed field becomes a crash in the loop.
Check your understanding