3. Functions
Define behaviors cleanly: parameters, defaults, typing, closures, and small utilities like decorators.
Q1 What are *args and **kwargs?
Answer: They allow a function to accept a variable number of arguments. *args collects extra positional arguments into a tuple, while **kwargs collects extra keyword arguments into a dictionary.
def func(a, b, *args, **kwargs):
print(f"args={args}, kwargs={kwargs}")
func(1, 2, 3, 4, x=5, y=6)
# args=(3, 4), kwargs={'x': 5, 'y': 6}
Q2 Why is it bad to use mutable objects (like a list) as default arguments?
Answer: Default arguments are created only once when the function is defined. A mutable default will be shared across all calls, leading to unexpected side effects. The standard practice is to use None as the default and create a new object inside the function.
def append_safe(item, bag=None):
if bag is None:
bag = []
bag.append(item)
return bag
Q3 What's the difference between a shallow copy and a deep copy?
Answer: A shallow copy duplicates the container but references the same nested objects; a deep copy recursively duplicates all nested objects.
import copy
original = [[1], [2]]
shallow = original.copy() # or list(original)
deep = copy.deepcopy(original)
original[0].append(99)
# shallow[0] now also has 99; deep[0] is unchanged
Q4 Explain Python's scope rules (LEGB).
Answer: Python searches for variables in this order:
Local: Inside the current function.
Enclosing: In the scope of any enclosing functions.
Global: At the top level of the module.
Built-in: In Python's built-in functions.
Q5 How do you annotate functions with types?
Answer: Use type hints on parameters and return values.
def greet(name: str) -> str:
return f"Hi {name}"
numbers: list[int] = [1, 2, 3]
maybe_name: str | None = None
Explanation: Type hints improve readability and tooling. They are not enforced at runtime; use tools like mypy or ruff for checking.
Q6 What are parameter kinds (positional-only, keyword-only)?
Answer: Use / to mark positional-only parameters and * to start keyword-only parameters.
def f(a, /, b, *, c):
return a, b, c
f(1, 2, c=3) # OK
f(a=1, b=2, c=3) # TypeError: a is positional-only
Q7 How do you unpack arguments when calling a function?
Answer: Use *iterable for positional args and **mapping for keyword args.
args = (1, 2)
kwargs = {"c": 3}
f(*args, **kwargs)
Q8 What are lambdas and when should you use them?
Answer: Anonymous, single-expression functions handy for short callbacks or keys to sorted, but avoid complex logic for readability.
square = lambda x: x * x
sorted_users = sorted(users, key=lambda u: (u["age"], u["name"]))
Q9 What are closures?
Answer: A closure is a function that captures variables from its enclosing scope, allowing state to persist without using global variables.
def make_counter():
count = 0
def inc():
nonlocal count
count += 1
return count
return inc
c = make_counter()
c(), c() # 1, 2
Q10 When to use global and nonlocal?
Answer: global assigns to a module-level variable; nonlocal assigns to a variable in an enclosing function scope.
total = 0
def add(n):
global total
total += n
def outer():
x = 0
def inner():
nonlocal x
x += 1
return x
return inner
Q11 What is a decorator?
Answer: A decorator takes a function and returns a new function adding behavior (e.g., logging, caching). Use functools.wraps to preserve metadata.
from functools import wraps
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_calls
def greet(name: str) -> str:
return f"Hi {name}"