Functions — Definition, Arguments, and the Many Kinds of Parameters
~25 min · function, def, arguments, kwargs, default
Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
Functions are first-class objects
In Python, functions are values. You can pass a function as an argument, return a function from another function, store one in a list or dict, and assign it to a name. def f(): pass creates a function object and binds the name f to it. This isn't a special case — it's the same kind of binding as x = 5, just with a function on the right side.
The four parameter kinds — and the / and * separators
A Python parameter can be: positional-only (callable only by position, separator /), positional-or-keyword (the default — callable either way), keyword-only (callable only by name, separator *), and variadic — *args for extra positionals, **kwargs for extra keywords. Most functions use just positional-or-keyword. Library authors use the separators to lock down API shape.
Default values — and the mutable trap (again)
Default values are evaluated once at function-definition time. For immutable defaults (numbers, strings, None, tuples) this is fine. For mutable defaults (list, dict, set) it's a bug magnet — the same object is reused on every call. The fix is the None sentinel pattern: def f(x=None): if x is None: x = []. We saw this in lesson 2; it's that important.
*args and **kwargs — both directions
In a function signature, *args packs extra positionals into a tuple, **kwargs packs extra keywords into a dict. At a call site, * and **unpack in the reverse direction: f(*my_list, **my_dict). Same syntax, opposite semantics depending on context.
Lambda — the small, anonymous function
lambda x: x * 2 is a single-expression function. It's the same as def f(x): return x * 2 but inline. Useful as the key argument to sorted, as a quick callback, or anywhere you'd assign a function to use it once and never again. Don't write multi-statement logic with lambdas — use def.
Principle: Type hints (covered in the typing track) are your best documentation for what a function expects. def send(to: str, body: str, *, retry: int = 3) -> bool: tells the reader more in 60 characters than three docstring paragraphs.
Code
Functions are first-class — pass, store, return·python
def double(x):
return x * 2
def triple(x):
return x * 3
# Pass as argument
def apply(fn, value):
return fn(value)
print(apply(double, 5)) # 10
print(apply(triple, 5)) # 15
# Store in a dict
ops = {"x2": double, "x3": triple}
print(ops["x2"](7)) # 14
# Return from a function
def multiplier(n):
def inner(x):
return x * n
return inner
times_5 = multiplier(5)
print(times_5(10)) # 50
The four parameter kinds + separators·python
# / marks the boundary: everything LEFT is positional-only
# * marks the boundary: everything RIGHT is keyword-only
def f(p1, p2, /, normal, *, kw1, kw2=10):
return p1, p2, normal, kw1, kw2
# Valid
print(f(1, 2, 3, kw1=4)) # (1, 2, 3, 4, 10)
print(f(1, 2, normal=3, kw1=4, kw2=99)) # (1, 2, 3, 4, 99)
# Invalid
try:
f(p1=1, p2=2, normal=3, kw1=4) # p1/p2 are positional-only
except TypeError as e:
print(e)
try:
f(1, 2, 3, 4) # kw1 must be keyword
except TypeError as e:
print(e)
Default values — the mutable trap·python
# DON'T
def bad(item, history=[]):
history.append(item)
return history
print(bad("a")) # ['a']
print(bad("b")) # ['a', 'b'] <- shared!
# DO
def good(item, history=None):
if history is None:
history = []
history.append(item)
return history
print(good("a")) # ['a']
print(good("b")) # ['b'] <- fresh
*args and **kwargs — packing and unpacking·python
# In a SIGNATURE: pack extras
def summary(name, *items, **flags):
print(f"name={name}, items={items}, flags={flags}")
summary("a", 1, 2, 3, debug=True, verbose=False)
# name=a, items=(1, 2, 3), flags={'debug': True, 'verbose': False}
# At a CALL SITE: unpack in the opposite direction
args = ("a", 1, 2, 3)
kwargs = {"debug": True}
summary(*args, **kwargs)
# name=a, items=(1, 2, 3), flags={'debug': True}
# Forwarding a generic call (decorator pattern preview)
def wrap(fn):
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
lambda — single-expression functions·python
# Sort a list of dicts by a field
people = [{"name": "a", "age": 30}, {"name": "b", "age": 25}]
print(sorted(people, key=lambda p: p["age"]))
# Filter + map (though comprehensions usually beat these)
nums = [1, 2, 3, 4, 5]
evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens) # [2, 4]
# Quick sort key with multiple criteria
records = [("b", 2), ("a", 3), ("a", 1), ("b", 1)]
# Sort by first, then second
print(sorted(records, key=lambda r: (r[0], r[1])))
# [('a', 1), ('a', 3), ('b', 1), ('b', 2)]
Write a function report(title, /, *items, sep=' | ', limit=None, **meta) where: title is positional-only; items collects extra positionals; sep and limit are keyword-only with defaults; meta collects everything else. The function should print title, then up to limit items joined by sep (or all items if limit is None), then any meta as key=value lines. Test with at least four call shapes — including report("a", 1, 2, 3, limit=2, color='red').
Progress
Progress is local-only — sign in to sync across devices.