JJoeven

Curriculum/Python

Scope, Defaults, and Extra Arguments

Local vs global names, default arguments, *args and **kwargs, and why a list default leaks agent memory.

beginner19 min17 / 37

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.

Pass in, return out
currentnext_scorenew score

Local names die when the function ends. Do not hide agent memory in a global or a list default.

Pass in, return out

This 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
  • args and *kwargs as extra inputs

Local vs global

Assignment inside a function makes a local name. The name outside does not change.

python
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:

python
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:

python
def add_event(event, history=[]):  class="tok-c"># BAD
    history.append(event)
    return history

Every 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.

DefaultSafe?Why
k=3YesNumbers cannot change in place
label="user"YesStrings cannot change in place
history=[]NoOne list is reused; append keeps old items
history=None then []YesEach 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.

  • *args means extra positional inputs (in order). It is a tuple — a fixed row of values.
  • kwargs means 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.

FormWhereMeaning
*argsin defExtra values in order, as a tuple
**kwargsin defExtra named values, as a dict
*rowin a callUnpack a list or tuple into order
**din a callUnpack a dict into names
python
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.
  • global to dodge passing state.
  • Calling fn(kwargs) instead of fn(**kwargs) — you pass one dict as the first argument.
  • Putting args after *kwargs in the definition (illegal order).
Live PythonOpen full playgroundpython
Output
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

Why is def add_event(event, history=[]) dangerous?