Functions, Scope and Closures: Interview Questions
50 min · Beginner
Need help? Ask a tutor on WhatsApp (opens in a new tab)Python function questions repeat in every round: "What is *args?", "Why did my default list grow?", "What is a closure?". This lesson gives each one a short, correct answer you can back with a three-line program.
Recap: *args, **kwargs and parameter order
*args collects extra positional arguments into a tuple. **kwargs collects extra keyword arguments into a dict. The names are only a habit; the stars do the work.
A function can accept any number of arguments. The order in a definition is fixed: normal parameters, defaults, *args, keyword-only parameters, then **kwargs.
def invoice(customer, *items, tax=0, **notes): print(customer, items) print(tax, notes)
invoice("Asha", "pen", "book", tax=5, gift=True)Example 1 · Unpack arguments with * and **
def total(*amounts, discount=0): return sum(amounts) - discount
print(total(100, 250, 50))print(total(*[100, 250, 50], discount=40))
def show(name, qty): print(name, qty)
show(**{"name": "pen", "qty": 3})400 360 pen 3
What this code does
*amountscollects positional values into a tuple. At the call,*[...]does the reverse and spreads a list into separate arguments.**{...}spreads a dict into keyword arguments, soshowreceivesnameandqtyby name.
"pen" and "book" land in the tuple items. tax comes after *items, so it can only be given by name. gift=True is left over, so it goes into the dict notes.
Recap: the default-argument trap
A default argument is a value given in the def line. Python evaluates it once, when the function is defined, and reuses that one object on every call.
A default list is made once and kept on the function. Each call that changes it changes the same list.
def add(item, bucket=[]): bucket.append(item) return bucket
print(add("pen"))print(add("book"))The fix is None as the default, and a new list inside the function.
Example 2 · The default trap and its fix
def bad(item, bucket=[]): bucket.append(item) return bucket
def good(item, bucket=None): if bucket is None: bucket = [] bucket.append(item) return bucket
print(bad(1), bad(2))print(good(1), good(2))[1, 2] [1, 2] [1] [2]
What this code does
badreturns the one list made whendefran, so both results are that list and it prints[1, 2] [1, 2].goodmakes a fresh list on every call that does not pass one.
The second call prints both items because it reused the first call's list. Use def add(item, bucket=None) and if bucket is None: bucket = [].
Recap: scope, closures and lambda
A closure is an inner function that keeps access to variables of the function that made it, even after that outer function has returned.
Python looks up a name in this order: Local, Enclosing, Global, Built-in (LEGB). To change an enclosing variable from an inner function, declare it nonlocal.
def make_counter(): count = 0 def step(): nonlocal count count += 1 return count return step
counter = make_counter()print(counter(), counter())A lambda is a small function with one expression.
Example 3 · Late binding in loops
funcs = [lambda: i for i in range(3)]print([f() for f in funcs])
fixed = [lambda i=i: i for i in range(3)]print([f() for f in fixed])[2, 2, 2] [0, 1, 2]
What this code does
- A lambda looks up
iwhen it runs, not when it is made. After the loopiis 2, so all three give 2. i=isaves the current value as a default when each lambda is created, so each one keeps its own number.
step remembers count from make_counter. Without nonlocal, count += 1 would raise UnboundLocalError, because Python treats count as a local name of step.
Next step
Check your understanding
12 quick questions