MOD-16 · Sequence #16 · Functions & Modules

Python Functions & Parameters

Functions are first-class objects in Python. Understanding positional arguments, keyword arguments, *args (variable positional), **kwargs (variable keyword), and the infamous mutable default argument trap is vital for software engineering.

Live Interactive Code Snippet (Python 3.12 CPython WASM) Open in Practice Mode →
def greet(*names, greeting="Hello"):
    for n in names:
        print(f"{greeting}, {n}!")

greet("Alice", "Bob", greeting="Welcome")

Core Principles & Mental Models

1. Call-by-Object-Reference (Pass by Assignment)

Function Parameter Passing Semantics

  • Python does NOT use pass-by-value or pass-by-reference. It uses "call-by-object-sharing" (pass-by-assignment).
  • The caller’s arguments are bound to parameter names in the local function scope.
  • Mutating a mutable argument (like a list) inside the function modifies the caller’s object in heap memory.
  • Reassigning the parameter name (`x = [1, 2]`) rebinds the local variable only, leaving the caller untouched.
def modify_list(lst):
    lst.append(99)  # Mutates caller's object!
    lst = [1, 2]    # Rebinding local variable does not affect caller

my_data = [10]
modify_list(my_data)
print("my_data after function:", my_data)  # [10, 99]
💡 Key Takeaway: In-place mutations affect the caller; rebindings within function scope do not.

2. Flexible Signatures: *args and **kwargs

Tuples of Positional & Dictionaries of Keyword Args

  • `*args` collects any excess positional arguments into a tuple.
  • `**kwargs` collects any excess keyword arguments into a dictionary.
def make_profile(name, *hobbies, **details):
    print(f"Name: {name}")
    print("Hobbies tuple:", hobbies)
    print("Details dict:", details)

make_profile("Alice", "Chess", "Piano", role="Student", gpa=3.9)
💡 Key Takeaway: Use *args and **kwargs to create flexible wrapper functions and decorators.

Interactive Walkthrough Examples

Keyword-Only Arguments (PEP 3102) (Intermediate)

Using the bare asterisk (*) to enforce keyword arguments.

def calculate_interest(principal, rate, *, compound_frequency=12):
    return principal * (1 + rate / compound_frequency) ** compound_frequency

# compound_frequency MUST be passed as a named keyword:
total = calculate_interest(1000, 0.05, compound_frequency=12)
print(f"Total: ${total:.2f}")

Explanation: Parameters after a bare `*` can only be passed as explicit keyword arguments.

Common Misconceptions & Exam Traps

Common Pitfall: The Mutable Default Argument Trap

Default argument expressions are evaluated once when the `def` statement executes, not on each call. The same list object is shared across all function calls.

Buggy Code
def add_item(item, basket=[]):  # Flawed: basket is created ONCE at function definition time!
    basket.append(item)
    return basket

print(add_item("apple"))   # ['apple']
print(add_item("banana"))  # ['apple', 'banana'] - ACCUMULATES across calls!
Correct Solution
def add_item(item, basket=None):
    if basket is None:
        basket = []  # Fresh list created on every call
    basket.append(item)
    return basket

print(add_item("apple"))   # ['apple']
print(add_item("banana"))  # ['banana']

Exam Trap Note: This is tested on virtually every undergraduate Python exam and coding assessment worldwide.

Conceptual Knowledge Self-Check Quiz

Q1: Why should you never use `def f(a, items=[])` in Python?

  1. It causes a SyntaxError
  2. The list is created only once when the function is defined, sharing state across calls
  3. Lists cannot be passed as arguments in Python
  4. Functions cannot have default parameters

Python evaluates default arguments once at definition time. Subsequent calls mutate the shared default list.

Algorithmic Practice Challenges

Idiomatic Best Practices

  • ✅ Always use `None` as the default value for mutable parameters (lists, dictionaries, sets).
  • ✅ Provide type hints and docstrings for all function signatures.

All 26 Curriculum Modules