Python Arguments and Scope: Parameters, LEGB, and Worked Examples

Trace how Python binds arguments, handles mutation and rebinding, and resolves names through LEGB. Runnable examples turn scope rules into predictable output.

KnowledgeGate Team

Exam prep & CS education

Updated 10 Oct 20265 min read

A function call binds objects to new local names, while nested functions read names through the LEGB lookup order. Argument binding, mutation, rebinding, and lookup are separate operations; tracing them in order makes both output and errors predictable.

Parameters and arguments: the vocabulary first

A parameter is a name written in a function definition. An argument is the value or expression supplied in a call. Consider this complete function:

python
def record_attempt(topic, score=0, *, reviewed=False):
    status = "reviewed" if reviewed else "pending"
    return topic, score, status

print(record_attempt("scope"))
print(record_attempt("scope", 8, reviewed=True))

Here, topic, score, and reviewed are parameters. In record_attempt("scope", 8, reviewed=True), "scope" and 8 are positional arguments, while reviewed=True is a keyword argument. score=0 and reviewed=False are defaults used when those arguments are omitted. Because reviewed appears after *, it is keyword-only.

The official Python tutorial on defining functions documents these call and default-parameter rules. The first call returns ("scope", 0, "pending"). The second returns ("scope", 8, "reviewed"), because the explicit positional and keyword arguments replace both defaults.

How Python binds positional, keyword, default, *args, and **kwargs

Read a call in a practical order: match positional arguments from left to right, apply explicit keyword arguments, fill omitted optional parameters from defaults, then collect any supported extras. A parameter cannot receive two values. Therefore, record_attempt("scope", topic="functions") raises TypeError because topic is assigned both positionally and by keyword.

python
def build_order(customer, *items, priority=False, **notes):
    return customer, items, priority, notes

result = build_order("Asha", "book", "pen", priority=True, city="Pune")

The exact result is ("Asha", ("book", "pen"), True, {"city": "Pune"}). customer gets "Asha", items gets the tuple ("book", "pen"), priority gets True, and notes gets the dictionary {"city": "Pune"}. The names args and kwargs are conventions, not Python keywords. Prefer explicit parameters when you already know the accepted inputs.

What is actually passed: mutation versus rebinding

Saying only that Python uses “pass by reference” hides the important behaviour. A parameter becomes a local name bound to the same object supplied by the caller. Mutation changes that shared object, while rebinding changes what the local name refers to.

python
def update(items, n):
    items.append(4)
    n += 1

nums = [1, 2, 3]
count = 3
update(nums, count)

After the call, nums is [1, 2, 3, 4] because append mutated the shared list. count remains 3 because n += 1 rebound only the local name n. Return a new value when the caller needs a scalar updated, and document intentional list or dictionary mutation. The Coding & Skills category is a useful next stop for structured programming practice.

Python scope and the LEGB lookup order

LEGB means Local, Enclosing, Global, Built-in. Python resolves a name from the nearest applicable scope, not from the place where a function happens to be called. The Python execution model gives the formal name-resolution rules.

python
tax = 18

def invoice(subtotal):
    discount = 10

    def total():
        fee = 50
        return subtotal * (1 - discount / 100) * (1 + tax / 100) + fee

    return total()

print(invoice(2000))

Inside total, fee=50 is local. subtotal=2000 and discount=10 are enclosing bindings created by invoice. tax=18 is global. The trace is 2000 * 0.90 = 1800.0, 1800.0 * 1.18 = 2124.0, then 2124.0 + 50 = 2174.0. A name such as round would come from the built-in scope, although this calculation does not need it.

Four LEGB rings for the invoice example: fee is local, subtotal and discount enclosing, tax global, and the total is 2174.0.

Rebinding names with nonlocal and global

nonlocal makes an assignment target an existing binding in the nearest enclosing function scope. global makes an assignment target the module-level binding. Neither declaration is required merely to read a name.

python
def make_counter(start=3):
    count = start

    def next_count(step=2):
        nonlocal count
        count += step
        return count

    return next_count

counter = make_counter()
print(counter())
print(counter())

The calls print 5 and 7. The enclosing count persists between calls. For contrast, if module-level calls = 0, a function that performs calls += 1 must first declare global calls; one call then changes that module binding to 1. In ordinary code, return values or small state-holding objects are often easier to test than hidden global writes.

Common failures: mutable defaults, shadowing, and local-name errors

Use Python Functions Deep Dive: *args, **kwargs, the Mutable Default Trap and LEGB Scope for the complete three-call mutable-default trace plus closures and late binding. At call time, the key binding rule is that a mutable default is created when def runs, so every omitted argument reuses that object. A signature such as def add(item, box=[]) therefore lets one call's append affect the next. Bind None and allocate inside the call instead:

python
def add(item, box=None):
    if box is None:
        box = []
    box.append(item)
    return box

Another trap starts with global score = 10, then reads score inside a function before assigning score = 20 there. Python treats score as local throughout that function, so the early read raises UnboundLocalError. Passing the value in and returning the replacement is cleaner than reflexively adding global.

Shadowing can also break familiar functions. After len = 4, the call len([1, 2]) raises TypeError: 'int' object is not callable; use a name such as length. A missing name raises NameError, while UnboundLocalError is the more specific case of a local name read before it has a value.

How exams and interviews test arguments and scope

Assessment questions usually ask for output prediction, error identification, parameter binding, default-object reuse, or the name selected by LEGB. Work through each shape until you can predict the result before running the code.

  1. Change the call to record_attempt(topic="scope", score=8, reviewed=True). Answer: all three arguments now bind by keyword, and the result is ("scope", 8, "reviewed").
  2. Predict the first two calls to counter(). Answer: they return 5 and 7; count is an enclosing binding updated through nonlocal.

  3. Explain why a list default can retain data between calls, then repair the signature. Answer: the list is created once when def runs; use box=None and create box = [] inside the function when needed.
  4. Repair the score example. Answer: write def update_score(score): score = 20; return score. Then score = update_score(10) produces 20.

For function definitions, return versus print, and composing calls, use Functions in Python: Parameters, Return Values, Scope and Examples. Argument binding, shared-object mutation, rebinding, and LEGB lookup are the next layer once those basics are secure.

Short version and the next step

  • Arguments supply objects.

  • Parameters bind local names.

  • Mutation can affect a shared mutable object.

  • Rebinding normally stays local.

  • LEGB decides where a read finds a name.

Rerun each example after changing one argument or one scope at a time, then return to the Python course for the full language sequence. If your next goal is coding rounds and interview practice, the Coding for Placements course is the more focused follow-on.