Functions, Scope & the Mutable-Default Trap
Define functions, pass default arguments, and understand local vs global scope — plus the mutable-default trap GATE tests directly.
What you'll learn
- Defining functions with positional, keyword, and default arguments, and using return
- Local vs global scope: assignment inside a function creates a local name unless declared global
- Default arguments are evaluated ONCE at definition, so a mutable default persists across calls
- Predicting the output of repeated calls to a function with a list default (a real 2026 question)
Before you start
The last lesson ended by pointing straight here: passing a list into a function makes the parameter another alias for the caller’s list, so the function can mutate it. That was the gentle version. Functions hide a sharper trap too, and GATE DA loves it — a mutable default argument (a list or dict) is created once, when the function is defined, not fresh on each call. So it quietly persists and accumulates across calls, carrying values from one invocation into the next.
This lesson builds functions and scope from the ground up, then walks the real GATE DA 2026
question that turns on exactly this. It is not mere exam trivia: this same mutable-default bug
silently corrupts state in production data pipelines, which is why linters like pylint flag
it on sight.
Defining functions and arguments
A function packages a computation. Its arguments can be positional, passed by keyword, or given a default value used when the caller omits them.
def power(base, exp=2): # exp has a default of 2
return base ** exp
power(5) # 25 -> exp defaults to 2
power(5, 3) # 125 -> positional: base=5, exp=3
power(exp=3, base=5) # 125 -> keyword arguments, order-free
A function returns None if it has no return. And everything after a return runs in the
caller, not the function — return hands control straight back.
Local vs global scope
A name assigned inside a function is local to that function — it does not leak out, and
it does not change a same-named variable outside, unless you declare it global.
x = 10
def f():
x = 5 # local x; the global x is untouched
return x
f() # 5
print(x) # 10 -> global x unchanged
You can read a global without declaring anything; you only need global when you want to
reassign it. Reading is free; rebinding needs permission.
The mutable-default trap
Here is the heart of the lesson. A default value is evaluated once, at the moment the def
runs — not each time you call. If that default is a mutable object (a list or dict), the
same object is reused on every call that does not supply its own, so it keeps whatever earlier
calls put into it.
Run it and watch the default accumulate:
def f(val, lst=[]):
lst.append(val)
return lst
print(f(1)) # first call, default list gets 1
print(f(2)) # SAME default list still holds the 1!
print(f(3, [])) # a fresh list is passed in, so just 3
print(f(4)) # back to the shared default (still has 1, 2)
prints, exactly:
[1]
[1, 2]
[3]
[1, 2, 4]
The 1 from the first call is still there on the second, and even on the fourth — because every
call that omits lst reaches the same list. The fix is to default to None and build the
list inside:
def f(val, lst=None):
if lst is None:
lst = [] # a NEW list every call
lst.append(val)
return lst
How GATE asks this
A predict-the-output MCQ: a function with a lst=[] (or d={}) default is called two or
three times, and you choose the printed result of a later call. The whole point is whether you
know the default is shared across calls, so the list carries over. Occasionally a NAT
asks for the length of the returned list after n calls. This appeared in GATE DA 2026.
Worked example — a real 2026 question
def f(val, lst=[]): lst.append(val) return lstWhat do
f(1), thenf(2), thenf(3, [])return?
The default list is created once when def f runs, so all calls that omit lst share it:
f(1)—lstis the shared default[]; append1→ returns[1]. The shared list is now[1].f(2)—lstis that same shared list, still[1]; append2→ returns[1, 2]. The earlier1persisted because it is the same object.f(3, [])— here the caller passes its own fresh list[], so the shared default is bypassed; append3→ returns[3].
So the outputs are [1], [1, 2], [3]. The jump from [1] to [1, 2] — with nothing in the
call seeming to carry the 1 forward — is the trap, and the answer to this GATE DA 2026 question.
A question to carry forward
You have now seen functions call other functions, each with its own private local scope. But here is the twist that unsettles every beginner: a function is allowed to call itself. Each self-call opens a brand-new local scope, so the variables never collide, and the calls stack up and then unwind. It sounds like it should loop forever. Here is the thread onward: how does a function that calls itself ever stop — and how do you trace the growing tower of calls it builds before it comes back down?
In one breath
- Arguments can be positional, keyword, or default (
def power(base, exp=2)); a function with noreturnreturnsNone. - Scope: assignment inside a function makes a local name; the global is untouched unless
you declare
globalto reassign it (reading a global is free). - The trap: a default is evaluated once at definition, so a mutable default
(
lst=[],d={}) is shared and accumulates across calls —f(1)→[1],f(2)→[1,2](GATE DA 2026). - Passing your own argument (
f(3, [])) bypasses the shared default. - Fix: default to
None, then build the list/dict inside. Immutable defaults are safe.
Practice
Quick check
Practice this in an interview
All questionsDefault argument values are evaluated once when the function is defined, not each time it is called. If the default is a mutable object like a list or dict, all calls that use the default share the same object — so mutations in one call persist into the next. The fix is to use None as the default and create the mutable object inside the function body.
Python passes references to objects, so a mutable argument (list, dict, set) can be modified inside a function and the change is visible to the caller. An immutable argument (int, str, tuple) cannot be mutated in place, so rebinding the local name only affects the local scope. The most common trap is using a mutable object as a default argument value, which is shared across all calls.
global declares that a name inside a function refers to the module-level variable, allowing reassignment. nonlocal does the same for the nearest enclosing function scope. Both should be used sparingly — they make control flow harder to reason about, and a class or closure that returns a value is usually a cleaner design.
Python resolves names by searching four scopes in order: Local, Enclosing, Global, then Built-in. The first match wins. Assignment in a scope always creates or modifies a name in that scope unless global or nonlocal overrides this.