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

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.

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.

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.

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:

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 seeming to carry the 1 forward — is the trap, and the answer to this GATE DA 2026 question.

Quick check

Quick check

0/7
Q1def f(val, lst=[]): lst.append(val); return lst. After f(1), f(2), f(3), what is len(f(3))? (integer)numerical answer — type a number
Q2x = 10; def g(): x = 5; return x. After calling g(), what does the global x equal? (integer)numerical answer — type a number
Q3def f(val, lst=[]): lst.append(val); return lst. What does f(2) return on the SECOND call (after one earlier f(1))?
Q4Which 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
Q5Which statements about Python functions and scope are TRUE? (select all that apply)select all that apply
Q6def power(base, exp=2): return base ** exp. What does power(3) return?
Q7The 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?

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.

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.

Explore further

Related lessons

Skip to content