3. Functions
Define behaviors cleanly: parameters, defaults, typing, closures, and small utilities like decorators.
Question: 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}
Question: 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
Question: 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
Question: 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.
Question: 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.
Question: 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
Question: 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)
Question: 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"]))
Question: 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
Question: When to use
global
andnonlocal
?
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
Question: 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}"