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
Functions look simple, but GATE DA hides one beautiful trap in them: 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. This lesson builds functions and scope from the ground up, then walks the real
2026 question that turns on exactly this. It is not just exam trivia: this same
mutable-default bug silently corrupts state in production data pipelines, and linters
like pylint flag it for that reason.
Defining functions and arguments
A function packages a computation. 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. Everything after a return executes
in the caller, not the function.
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.
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 in it.
Run it and watch the default accumulate:
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 seeming to carry the 1 forward — is the trap, and the answer to this GATE DA
2026 question.
Quick check
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.