Functions And Functional Basics
Function Arguments
Give a parameter a fallback value, making it optional at call time:
Jr Codex Python Notes
Level: Intermediate Prerequisites: Chapter 1 Time to complete: ~25 minutes
Table of Contents
- Default Arguments
- The Mutable Default Argument Trap
*args— Variable Positional Arguments**kwargs— Variable Keyword Arguments- Combining Everything
- Keyword-Only & Positional-Only Arguments
- Summary & Next Steps
1. Default Arguments
Give a parameter a fallback value, making it optional at call time:
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet("Alice")) # "Hello, Alice!" — uses default
print(greet("Bob", "Hi")) # "Hi, Bob!" — overrides default
print(greet("Charlie", greeting="Hey")) # "Hey, Charlie!" — keyword formRule: parameters with defaults must come after parameters without defaults:
def broken(name="Anonymous", age): # SyntaxError: non-default argument follows default argument
pass
def fixed(age, name="Anonymous"): # ✓ correct order
pass2. The Mutable Default Argument Trap
One of Python's most infamous gotchas — a default value is evaluated once, when the function is defined, not each time it's called.
def add_item(item, cart=[]): # ✗ DANGER: mutable default
cart.append(item)
return cart
print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['apple', 'banana'] — NOT a fresh list! Same object reused.Why This Happens
─────────────────────────────────────────
def add_item(item, cart=[]):
↑
This list is created ONCE, at function definition time,
and reused across EVERY call that doesn't pass its own cart.
─────────────────────────────────────────
The fix: use None as the default, and create the mutable object inside the function body:
def add_item(item, cart=None): # ✓ safe
if cart is None:
cart = []
cart.append(item)
return cart
print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['banana'] — fresh list each timeThis is one of the most common real-world Python bugs — internalize the fix.
3. *args — Variable Positional Arguments
Use *args to accept any number of positional arguments — they arrive packed into a tuple.
def total(*args):
print(type(args)) # <class 'tuple'>
return sum(args)
print(total(1, 2, 3)) # 6
print(total(1, 2, 3, 4, 5)) # 15
print(total()) # 0 — works with zero args tooargs is just a conventional name — the * is what matters:
def show(*numbers):
for n in numbers:
print(n)
show(10, 20, 30)4. **kwargs — Variable Keyword Arguments
Use **kwargs to accept any number of keyword arguments — they arrive packed into a dictionary.
def build_profile(**kwargs):
print(type(kwargs)) # <class 'dict'>
return kwargs
profile = build_profile(name="Alice", age=25, city="Boston")
print(profile) # {'name': 'Alice', 'age': 25, 'city': 'Boston'}
def print_info(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_info(name="Bob", role="Engineer")5. Combining Everything
Python enforces a strict order when mixing parameter types:
def full_example(a, b, *args, c=10, **kwargs):
print("a, b:", a, b)
print("args:", args)
print("c:", c)
print("kwargs:", kwargs)
full_example(1, 2, 3, 4, 5, c=99, x=100, y=200)
# a, b: 1 2
# args: (3, 4, 5)
# c: 99
# kwargs: {'x': 100, 'y': 200}Required Parameter Order
─────────────────────────────────────────
def f(positional, *args, keyword_default=value, **kwargs):
↑ ↑ ↑ ↑
required extra optional extra
positional positional keyword-only keyword
─────────────────────────────────────────
The * and ** Also Unpack at the Call Site
def add(a, b, c):
return a + b + c
numbers = [1, 2, 3]
print(add(*numbers)) # unpacks list into 3 positional args → 6
values = {"a": 1, "b": 2, "c": 3}
print(add(**values)) # unpacks dict into keyword args → 6This "unpacking at the call site" pattern shows up constantly — passing a config dict straight into a function, forwarding args through a wrapper (foundational for decorators, Module 5).
6. Keyword-Only & Positional-Only Arguments
Python lets you force certain arguments to be passed only by keyword (or only by position) — useful for API clarity.
# Keyword-only: anything after a bare * must be passed by keyword
def create_user(name, *, age, role="user"):
return f"{name}, {age}, {role}"
create_user("Alice", age=25) # ✓ works
create_user("Alice", 25) # ✗ TypeError — age must be a keyword arg
# Positional-only: anything before / must be passed positionally (Python 3.8+)
def divide(a, b, /):
return a / b
divide(10, 2) # ✓ works
divide(a=10, b=2) # ✗ TypeError — a and b must be positional| Syntax | Effect |
|---|---|
def f(a, *, b): | a positional or keyword; b keyword-only |
def f(a, /, b): | a positional-only; b positional or keyword |
This mostly matters for library/API design — forcing keyword args (like age= above) makes call sites self-documenting and prevents accidental argument-order bugs.
7. Summary & Next Steps
Key Takeaways
- Default arguments make parameters optional — but never use a mutable object (list, dict) as a default; use
Noneand initialize inside the function. *argscollects extra positional arguments into a tuple;**kwargscollects extra keyword arguments into a dict.- The same
*/**syntax unpacks collections into arguments at the call site —add(*[1,2,3]). *alone in a signature forces everything after it to be keyword-only;/forces everything before it to be positional-only.
Concept Check
- Why is
def add_item(item, cart=[]):dangerous, and what's the fix? - What data structure does
*argsproduce inside the function? What about**kwargs? - Given
def f(a, *, b):, isf(1, 2)valid? Why or why not?
Next Chapter
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index