Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Functions & Scope
PYTHON

Functions & Scope

Practice 5 Python questions covering functions, parameters, scope, closures, LEGB resolution, and related concepts.

1. What happens when a mutable object is used as a default parameter in a Python function?

def add_item(item, target=[]):
    target.append(item)
    return target

print(add_item(1))
print(add_item(2))

Output: [1] followed by [1, 2]

Default arguments in Python are evaluated exactly once — at function definition time, not on every call. The [] you write in the signature is a single list object, created when the def statement runs, and that same object is reused for every subsequent call that doesn’t pass a target.

So the first call add_item(1) appends to the default list, which now holds [1]. The second call add_item(2) uses the same list — still holding the 1 from before — and appends 2. Output: [1] then [1, 2].

This is the famous mutable-default-argument bug. The state “leaks” between calls, which is almost never what you want.

The standard fix is to default to None and create a fresh list inside the function:

def add_item(item, target=None):
    if target is None:
        target = []
    target.append(item)
    return target

Now every call that omits target gets a brand-new list. The interview takeaway: defaults are evaluated once; never use a mutable literal as a default.

Answer:

[1] followed by [1, 2]

Default arguments in Python are evaluated exactly once — at function definition time, not on every call. The [] you write in the signature is a single list object, created when the def statement runs, and that same object is reused for every subsequent call that doesn’t pass a target.

So the first call add_item(1) appends to the default list, which now holds [1]. The second call add_item(2) uses the same list — still holding the 1 from before — and appends 2. Output: [1] then [1, 2].

This is the famous mutable-default-argument bug. The state “leaks” between calls, which is almost never what you want.

The standard fix is to default to None and create a fresh list inside the function:

def add_item(item, target=None):
    if target is None:
        target = []
    target.append(item)
    return target

Now every call that omits target gets a brand-new list. The interview takeaway: defaults are evaluated once; never use a mutable literal as a default.

2. What is the scope resolution order that Python follows for variable lookups?

Answer: LEGB — Local → Enclosing → Global → Built-in.

When Python needs to resolve a name, it searches the scopes in a fixed order:

  1. Local — the current function’s namespace.
  2. Enclosing — the namespaces of outer functions that wrap the current one (for nested functions/closures).
  3. Global — the module level.
  4. Built-in — the builtin namespace (len, print, range, etc.).

The search stops at the first scope that contains the name. If none does, you get a NameError.

A subtle point worth knowing: assignment changes scope. If a function assigns to a variable, that variable is local to the function (unless declared global or nonlocal) — even if an outer scope has the same name. That’s why a line like print(x); x = 1 raises UnboundLocalError when x is later assigned: Python sees the assignment and marks x local for the whole function, so the print finds no local x yet.

The interview answer is simply: LEGB, in that exact order.

Answer:

LEGB — Local → Enclosing → Global → Built-in.

When Python needs to resolve a name, it searches the scopes in a fixed order:

  1. Local — the current function’s namespace.
  2. Enclosing — the namespaces of outer functions that wrap the current one (for nested functions/closures).
  3. Global — the module level.
  4. Built-in — the builtin namespace (len, print, range, etc.).

The search stops at the first scope that contains the name. If none does, you get a NameError.

A subtle point worth knowing: assignment changes scope. If a function assigns to a variable, that variable is local to the function (unless declared global or nonlocal) — even if an outer scope has the same name. That’s why a line like print(x); x = 1 raises UnboundLocalError when x is later assigned: Python sees the assignment and marks x local for the whole function, so the print finds no local x yet.

The interview answer is simply: LEGB, in that exact order.

3. What is the correct keyword used to modify an outer non-global variable inside a nested function?

Answer: nonlocal.

Python’s scoping rules make a subtle but crucial distinction. Inside a nested function, you can read a variable from an enclosing function without any declaration. But if you try to assign to it, Python assumes you’re creating a new local variable — unless you say otherwise.

  • global declares that a name refers to the module-level (global) scope.
  • nonlocal declares that a name refers to a variable in the nearest enclosing, non-global scope — i.e., an outer function’s local variable.

Example:

def outer():
    x = 10
    def inner():
        nonlocal x
        x = 20
    inner()
    print(x)   # 20 — changed by inner

Without nonlocal, x = 20 inside inner would create a brand-new local x, leaving the outer x untouched.

The interview answer: nonlocal is the keyword for binding to an enclosing (but not global) scope.

Answer:

nonlocal.

Python’s scoping rules make a subtle but crucial distinction. Inside a nested function, you can read a variable from an enclosing function without any declaration. But if you try to assign to it, Python assumes you’re creating a new local variable — unless you say otherwise.

  • global declares that a name refers to the module-level (global) scope.
  • nonlocal declares that a name refers to a variable in the nearest enclosing, non-global scope — i.e., an outer function’s local variable.

Example:

def outer():
    x = 10
    def inner():
        nonlocal x
        x = 20
    inner()
    print(x)   # 20 — changed by inner

Without nonlocal, x = 20 inside inner would create a brand-new local x, leaving the outer x untouched.

The interview answer: nonlocal is the keyword for binding to an enclosing (but not global) scope.

4. What happens if you modify a global variable inside a function without the global declaration?

Answer: If you assign to the name, Python creates a new local variable; if you reference it before assigning, you get UnboundLocalError.

Python decides a variable’s scope by where it’s assigned, not where it’s used. If a function contains any assignment to a name, that name is treated as local for the entire function — even lines before the assignment.

Two cases follow:

  • Assignment with no prior reference: x = 5 inside the function creates a local x. The global x is untouched. No error — but no global modification either.
  • Reference before assignment: if the function does print(x) and later x = 5, Python has already marked x as local. The print runs before the local exists, so it raises UnboundLocalError — not NameError, even though a global x exists.

To actually modify the global, you must declare global x at the top of the function. The interview answer: assignment makes it local (or raises UnboundLocalError when referenced before assignment); without global, the global stays untouched.

Answer:

If you assign to the name, Python creates a new local variable; if you reference it before assigning, you get UnboundLocalError.

Python decides a variable’s scope by where it’s assigned, not where it’s used. If a function contains any assignment to a name, that name is treated as local for the entire function — even lines before the assignment.

Two cases follow:

  • Assignment with no prior reference: x = 5 inside the function creates a local x. The global x is untouched. No error — but no global modification either.
  • Reference before assignment: if the function does print(x) and later x = 5, Python has already marked x as local. The print runs before the local exists, so it raises UnboundLocalError — not NameError, even though a global x exists.

To actually modify the global, you must declare global x at the top of the function. The interview answer: assignment makes it local (or raises UnboundLocalError when referenced before assignment); without global, the global stays untouched.

5. What is the default return value of a Python function that executes without an explicit return statement?

Answer: None.

If a function’s body runs to the end without hitting a return (or a return with no value), Python implicitly returns None.

def f():
    x = 1

print(f())   # None

Every Python function returns something — there’s no “void” concept like in C/Java. Functions that conceptually return nothing actually return the None object. This is why checking the result of a mutating method (list.append returns None) is a classic bug: the value is silently discarded.

The interview answer: None — implicit, always.

Answer:

None.

If a function’s body runs to the end without hitting a return (or a return with no value), Python implicitly returns None.

def f():
    x = 1

print(f())   # None

Every Python function returns something — there’s no “void” concept like in C/Java. Functions that conceptually return nothing actually return the None object. This is why checking the result of a mutating method (list.append returns None) is a classic bug: the value is silently discarded.

The interview answer: None — implicit, always.

My Private Notes

Notes are auto-saved locally to this device.