C.W.K.
Stream
Lesson 04 of 07 · published

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)]

External links

Exercise

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.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.