Functions & Scope

15 questions found

What is the difference between positional arguments and keyword arguments when calling a function?

Beginner
Positional arguments are matched to parameters by ORDER; keyword arguments are matched by explicit NAME (param=value), letting you pass arguments out of order and making the call site more self-documenting — a function call can freely mix both, positional first.
def greet(name, greeting):
    return f"{greeting}, {name}!"

greet("Sam", "Hello")               # positional
greet(name="Sam", greeting="Hi")    # keyword, order doesn't matter
greet("Sam", greeting="Hey")        # mixed
Real-world example Making a function call with many parameters more readable by using keyword arguments for less-obvious values.

Common follow-ups: Can you have a positional argument AFTER a keyword argument in the same call?

Data Types & Structures

How do default parameter values work, and when are they evaluated?

Beginner
A default value (`def func(param=default):`) is used when the caller omits that argument — critically, the default expression is evaluated ONCE, at FUNCTION DEFINITION time, not on every call, which matters for mutable defaults.
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

print(greet("Sam"))              # 'Hello, Sam!' -- uses the default
print(greet("Sam", "Hi"))        # 'Hi, Sam!' -- default overridden
Real-world example Providing a sensible default value for an optional configuration parameter, like a default timeout or retry count.

Common follow-ups: Why is using a mutable object (like a list) as a default value considered a classic Python pitfall?

Data Types & Structures

What is Python's LEGB rule for variable scope resolution, and what does each letter stand for?

Intermediate
LEGB describes the order Python searches for a variable name: Local (the current function), Enclosing (any outer function, for closures), Global (the module level), and Built-in (Python's built-in names like len or print) — Python looks in each scope in that order and uses the FIRST match found.
x = "global"
def outer():
    x = "enclosing"
    def inner():
        x = "local"
        print(x)  # 'local' -- found in Local scope first
    inner()
outer()
Real-world example Understanding exactly which 'x' a function refers to when the same name exists at multiple scope levels.

Common follow-ups: What happens if a variable name isn't found in ANY of the four LEGB scopes?

OOP

Why do you need the 'global' keyword to MODIFY a global variable from inside a function, but not to simply READ it?

Intermediate
Without 'global', assigning to a name inside a function creates a NEW LOCAL variable (shadowing the global one) rather than modifying the global — reading a global variable works fine without any declaration, since Python only needs the 'global' keyword to know your INTENT to assign to (not just read) the outer name.
counter = 0

def increment():
    global counter  # required to MODIFY the global, not just read it
    counter += 1

def read_only():
    print(counter)  # works fine without 'global', since we're only reading

increment()
print(counter)  # 1
Real-world example Modifying a module-level counter or shared state variable from inside a function, correctly signaling intent to the interpreter.

Common follow-ups: How does the 'nonlocal' keyword serve a similar but distinct purpose for ENCLOSING (not global) scope?

Comprehensions & Generators

How does the 'nonlocal' keyword let a nested function modify a variable in its ENCLOSING (not global) scope?

Intermediate
'nonlocal' tells Python that an assignment inside a nested function should modify the variable in the nearest ENCLOSING function's scope (not create a new local, and not reach all the way to global) — essential for implementing stateful closures, like a counter that persists across calls to an inner function.
def make_counter():
    count = 0
    def increment():
        nonlocal count  # modifies the ENCLOSING 'count', not global or a new local
        count += 1
        return count
    return increment

counter = make_counter()
print(counter())  # 1
print(counter())  # 2
Real-world example Building a stateful closure (like a counter or accumulator) where the inner function needs to persist and modify enclosing state across calls.

Common follow-ups: Why would 'global' NOT work correctly if used instead of 'nonlocal' in this exact scenario?

functools & Functional Programming Tools

How do *args and **kwargs let a function accept an arbitrary number of positional and keyword arguments?

Intermediate
*args collects any extra POSITIONAL arguments into a tuple; **kwargs collects any extra KEYWORD arguments into a dict — letting a function accept a flexible, variable number of arguments beyond its explicitly named parameters, commonly used for wrapper functions or highly generic APIs.
def flexible_function(required, *args, **kwargs):
    print(f"required={required}")
    print(f"args={args}")
    print(f"kwargs={kwargs}")

flexible_function(1, 2, 3, name="Sam", age=30)
# required=1, args=(2, 3), kwargs={'name': 'Sam', 'age': 30}
Real-world example Writing a generic wrapper or decorator function that needs to forward ANY arguments through to the wrapped function unchanged.

Common follow-ups: How do you UNPACK a list and dict back into a function call using * and ** at the CALL site, the reverse operation?

Decorators

Why is using a mutable default argument (like an empty list) a classic Python bug, and how do you avoid it?

Advanced
Since default values are evaluated ONCE at function definition time, a mutable default (like []) is the SAME object shared across EVERY call that uses the default — mutating it in one call PERSISTS into future calls, silently accumulating state; the fix is to default to None and create a fresh mutable object INSIDE the function body.
def add_item(item, items=[]):  # BUG: same list shared across all calls!
    items.append(item)
    return items

print(add_item("a"))  # ['a']
print(add_item("b"))  # ['a', 'b'] -- unexpectedly retained from the previous call!

# Fixed version:
def add_item_fixed(item, items=None):
    if items is None:
        items = []  # a NEW list created fresh, every call
    items.append(item)
    return items
Real-world example Debugging a mysterious bug where a function's 'default' list or dict argument seems to accumulate values across unrelated calls.

Common follow-ups: Does this same mutable-default pitfall apply to using a dict or a custom mutable object as a default, not just a list?

Debugging & Profiling

How do keyword-only arguments (using a bare '*' in the parameter list) force certain parameters to be passed by name only?

Advanced
Placing a bare '*' in the parameter list marks every parameter AFTER it as keyword-only — callers CANNOT pass them positionally, which improves call-site clarity for parameters whose meaning isn't obvious from a bare value alone (like a boolean flag).
def create_user(name, *, is_admin=False, send_welcome_email=True):
    ...

create_user("Sam", is_admin=True)          # OK: keyword
# create_user("Sam", True)                  # Error: is_admin must be passed by keyword
Real-world example Forcing boolean or easily-confused flag parameters to always be passed by explicit keyword, preventing a call like create_user('Sam', True, False) from being ambiguous.

Common follow-ups: How do positional-only parameters (using a bare '/' instead) serve the OPPOSITE purpose?

Type Hints

How do positional-only parameters (using a bare '/' in the parameter list, Python 3.8+) restrict how certain parameters can be passed?

Advanced
Placing a bare '/' in the parameter list marks every parameter BEFORE it as positional-only — callers CANNOT pass them by keyword name, useful for parameters whose NAME isn't meant to be part of the function's stable public API (letting you rename it later without breaking callers who used the keyword form).
def divide(a, b, /):
    return a / b

divide(10, 2)          # OK: positional
# divide(a=10, b=2)    # Error: 'a' and 'b' are positional-only, can't use as keywords
Real-world example Preventing callers from depending on a parameter's exact NAME as part of the function's API contract, giving you freedom to rename it later.

Common follow-ups: How would you combine BOTH '/' and '*' in the same function signature to have positional-only, normal, AND keyword-only parameters?

Type Hints

How does a closure capture variables from its enclosing scope, and what pitfall occurs when creating multiple closures inside a loop?

Advanced
A closure captures enclosing variables BY REFERENCE (not by value at creation time), meaning it sees whatever the variable's value is when the closure ACTUALLY RUNS — creating multiple closures in a loop that all reference the SAME loop variable means they all end up seeing its FINAL value, a classic and common bug.
functions = []
for i in range(3):
    functions.append(lambda: i)  # all three closures capture the SAME 'i' variable

print([f() for f in functions])  # [2, 2, 2] -- NOT [0, 1, 2] as might be expected!

# Fixed with a default argument, evaluated at DEFINITION time per-iteration:
functions_fixed = [lambda i=i: i for i in range(3)]
print([f() for f in functions_fixed])  # [0, 1, 2]
Real-world example Debugging a classic closure bug where multiple queued callbacks unexpectedly all report the SAME final loop value.

Common follow-ups: Why does adding a default argument (i=i) specifically fix this closure-capture bug?

Comprehensions & Generators

Showing 1–10 of 15