datarekha

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.

7 min read Intermediate GATE DA Lesson 49 of 122

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.

GLOBAL scopex = 10LOCAL scope of f()x = 5 → creates a NEW local x; global x stays 10global x; x = 5 → now rebinds the GLOBAL x to 5
Assignment inside a function is local by default; the global keyword opts into changing the outer name.
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.

def f(val, lst=[]): lst.append(val); return lstthe default [] is created ONCE and sharedf(1)[1]shared list:[1]f(2)[1, 2]SAME list persisted![1, 2]f(3, [])[3]fresh list passed in,default untouchedPassing your own list sidesteps the shared default entirely.
The default list survives between calls — until a call supplies its own list.

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 lst

What do f(1), then f(2), then f(3, []) return?

The default list is created once when def f runs, so all calls that omit lst share it:

  • f(1)lst is the shared default []; append 1 → returns [1]. The shared list is now [1].
  • f(2)lst is that same shared list, still [1]; append 2 → returns [1, 2]. The earlier 1 persisted because it is the same object.
  • f(3, []) — here the caller passes its own fresh list [], so the shared default is bypassed; append 3 → 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 no return returns None.
  • Scope: assignment inside a function makes a local name; the global is untouched unless you declare global to 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

0/7
Q1Recall: which statements about Python functions and scope are TRUE? (select all that apply)select all that apply
Q2Recall: def power(base, exp=2): return base ** exp. What does power(3) return?
Q3Trace: x = 10; def g(): x = 5; return x. After calling g(), what does the global x equal? (integer)numerical answer — type a number
Q4Trace: def f(val, lst=[]): lst.append(val); return lst. What does f(2) return on the SECOND call (after one earlier f(1))?
Q5Apply: def f(val, lst=[]): lst.append(val); return lst. You call f(1), then f(2), then f(3) in order, each omitting lst. What is the length of the list the THIRD call returns? (integer)numerical answer — type a number
Q6Apply: which calls return a list of length 1, given def f(val, lst=[]): lst.append(val); return lst, called in this exact order? (select all that apply)select all that apply
Q7Create: the trap also applies to dict defaults. def memo(k, v, cache={}): cache[k] = v; return cache. You call memo('a', 1) then memo('b', 2). What does the SECOND call return?

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Practice this in an interview

All questions
What is the mutable default argument trap in Python, and how do you fix it?

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

How does Python's mutable vs immutable distinction affect function arguments and default values?

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.

What do the global and nonlocal keywords do, and when should you use them?

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.

Explain Python's LEGB scope rule with an example.

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.

Related lessons

Explore further

Skip to content