Functions

Define functions with argument lists that read well — and meet the mutable default that bites every Python programmer exactly once.

09of 24
≈ 14Min read
5Parts

Positional, keyword, default

A call passes positional arguments first and keyword arguments after. Defaults make an argument optional, and a keyword argument at the call site is documentation the reader gets for free: send(..., urgent=True) says more than send(..., True).

py
def send(to, subject, body="", urgent=False):
    ...

send("a@b.c", "Hi")                        # positionally
send(to="a@b.c", subject="Hi")             # by keyword — order no longer matters
send("a@b.c", subject="Hi", urgent=True)   # mixed: positional first

# defaults are evaluated ONCE, when the def runs — not on each call

The mutable default

The default expression is evaluated once, when the def statement runs. If it is a list, a dict, a set or anything else mutable, every call that relies on the default shares one object — and it accumulates.

The rule is simple and absolute: default to None, and build the real default inside.

the classic bug
def add(item, basket=[]):       # the SAME list, for every call, forever
    basket.append(item)
    return basket

add("a")      # ['a']
add("b")      # ['a', 'b']   ← not a fresh basket

def add(item, basket=None):     # the fix, every time
    if basket is None:
        basket = []
    basket.append(item)
    return basket

args and *kwargs

The star in a definition collects; the star at a call site spreads. It is the same symbol doing opposite jobs, and once you see that, the whole notation stops being mysterious.

py
def log(level, *messages, **fields):
    #   level     one positional
    #   messages  a TUPLE of any extra positional arguments
    #   fields    a DICT of any extra keyword arguments
    print(level, " ".join(messages), fields)

log("info", "user", "logged in", user_id=7)
# info user logged in {'user_id': 7}

args = ["a", "b"]
opts = {"user_id": 7}
log("info", *args, **opts)        # unpacking, at the call site

Forcing the call to be readable

* in the parameter list means "everything after this must be named". Use it for flags and options — resize(img, True, False, True) is unreadable at the call site and unchangeable afterwards, because any caller may be depending on the order.

/ means "everything before this cannot be named", which frees you to rename those parameters later. Most standard-library functions use it.

py
def crop(image, /, width, height, *, keep_ratio=False):
    ...

# image       positional ONLY    — before the /
# width, height  either
# keep_ratio  keyword ONLY       — after the *

crop(img, 100, 200, keep_ratio=True)     # fine
crop(image=img, width=100, height=200)   # TypeError: image is positional-only
crop(img, 100, 200, True)                # TypeError: keep_ratio is keyword-only

Returning

A function with no return returns None. A return with several values returns a tuple, which the caller unpacks — and that is idiomatic Python rather than a trick.

py
def divide(a, b):
    if b == 0:
        return None, "cannot divide by zero"
    return a / b, None          # a tuple: parentheses optional

value, error = divide(10, 2)

def nothing():
    pass                        # returns None — every function returns something

Practice