Python functions look simple, so candidates often skim them and miss the output traps interviewers reuse. Three rules cause most surprises: defaults are created at function definition time, extra arguments are packed in specific containers, and names follow LEGB lookup. Each can change the answer to a two-line snippet.
Positional, keyword, and default arguments
A positional argument binds by position. A keyword argument binds by parameter name. A default supplies a value when the caller omits that parameter.
def greet(name, message="Hello"):
return f"{message}, {name}"
greet("Asha") # "Hello, Asha"
greet("Asha", "Welcome") # "Welcome, Asha"
greet(name="Asha", message="Hi") # "Hi, Asha"Parameters without defaults must normally come before parameters with defaults in the same positional-or-keyword group. The deeper rule is that Python evaluates a default expression once, when the def statement executes. It stores that object with the function. The expression is not evaluated again for every call.
An immutable string default rarely exposes this distinction because the function cannot alter the string in place. A mutable list makes it visible immediately.
The mutable default argument trap, traced
Consider this function:
def add(item, target=[]):
target.append(item)
return targetThere is one list in add.__defaults__. Calls that omit target all receive a reference to that same list.
add(1)appends1. The shared list becomes[1], and the call returns[1].add(2)appends2to the existing list. It becomes[1, 2], not[2].add(3)appends3to that same object. It becomes[1, 2, 3].
The output is therefore:
[1]
[1, 2]
[1, 2, 3]Use None as a sentinel when omission should create fresh state:
def add(item, target=None):
if target is None:
target = []
target.append(item)
return targetNow the first add(1) creates a new list and returns [1]. A separate add(2) creates another new list and returns [2]. A caller can still pass an existing list deliberately, and the function will append to that object.
![The function object add with __defaults__ pointing to one shared list that three calls grow from [1] to [1, 2] to [1, 2, 3], beside the None-sentinel version creating a fresh empty list for each omitted target.](https://kgai.blob.core.windows.net/blog-assets/blog_asset_1784075731549_23z4ra.jpg)
Python Output-Based Questions extend this object-sharing model beyond function defaults.
Packing and unpacking with *args and **kwargs
In a function definition, *args packs extra positional arguments into a tuple. **kwargs packs extra keyword arguments into a dictionary. The names args and kwargs are conventions; the stars create the behaviour.
def f(a, *args, **kwargs):
print(a)
print(args)
print(kwargs)
f(1, 2, 3, x=4, y=5)The bindings are exact:
a = 1args = (2, 3)kwargs = {'x': 4, 'y': 5}
Python's full parameter order can include positional-only parameters before /, then positional-or-keyword parameters, *args, keyword-only parameters, and finally **kwargs.
The same symbols unpack values at the call site:
values = [2, 3]
options = {"x": 4}
f(*values, **options)This is equivalent to f(2, 3, x=4), so a = 2, args = (3,), and kwargs = {'x': 4}. A one-item tuple needs the comma, which is why the representation is (3,).
Unpacking does not relax duplicate-binding rules. If a positional argument already filled a and the unpacked keywords also contain a, the call raises TypeError because the function received two values for one parameter.
LEGB scope and the assignment trap
Python resolves an unqualified name in this order:
Local: the current function.
Enclosing: any lexically enclosing function scopes, from inner to outer.
Global: the module.
Built-in: names supplied by Python, such as
len.
Trace a closure lookup:
x = "global"
def outer():
x = "enclosing"
def inner():
print(x)
inner()
outer()inner has no local x. Its enclosing outer scope has x = "enclosing", so lookup stops there and prints enclosing. The global value is never needed.

Assignment adds an important compile-time scope decision:
count = 0
def inc():
count += 1Calling inc() raises UnboundLocalError. Because the function assigns to count, Python treats count as local throughout inc(). The right side tries to read that local before it has a value.
If the intention is to update the module variable, declare global count. If the variable belongs to an enclosing function, declare it nonlocal in the nested function. Often a cleaner design is to return the new value or store state in an object rather than modify a global.
First-class functions, closures, and late binding
Functions are objects. You can store one in a variable, pass it to another function, and return it from one. A nested function that keeps access to an enclosing variable is a closure.
def twice(fn, value):
return fn(fn(value))
def make_adder(n):
def adder(x):
return x + n
return adder
add5 = make_adder(5)
print(twice(add5, 1)) # 11make_adder returns adder, which still reads n from the call that built it, so add5(1) is 6. Handing add5 to twice applies it to its own result and prints 11. That returned adder is the closure: it outlives make_adder and carries n = 5 with it.
Loop-created closures expose another output trap:
funcs = [lambda: i for i in range(3)]
print([fn() for fn in funcs])The lambdas look up i when called, after the loop has finished with i = 2. All three therefore return 2, producing [2, 2, 2].
Capture the current value through a default:
funcs = [lambda i=i: i for i in range(3)]
print([fn() for fn in funcs]) # [0, 1, 2]Here each lambda's default is evaluated when that lambda is created. The Python interview questions for freshers provide more chances to separate definition time from call time.
Function traps to check quickly
A mutable default is shared across calls that omit the argument.
Loop closures use late lookup unless you capture the current value.
Assigning to a name makes it local unless
globalornonlocalsays otherwise.Naming a variable
list,dict, orstrshadows a useful built-in.Mutating a passed list changes the caller-visible object; rebinding a local name does not replace the caller's variable.
*argsis a tuple and**kwargsis a dictionary, not two special argument types.
Use the Placement Preparation category to connect these output questions with the wider interview syllabus.
The short version and your next step
Defaults are evaluated once, *args and **kwargs pack extra arguments, names resolve through LEGB, and an assignment can make a name local before the function runs. For the classic traces, remember [1], [1, 2], [1, 2, 3] for the shared default and enclosing for the LEGB example.
KnowledgeGate's question bank holds about 450 Python questions for this kind of drill. Work through them with the Python Programming course, then use the Mera Placement Hoga bundle to practise the same reasoning under interview conditions.




