Arguments: positional, named, default, and the trap in the default
Four ways to hand a value to a function
def send(message, to, retries=3, urgent=False):
...
send("hello", "asha@example.com") # positional
send("hello", to="asha@example.com") # mixed
send(to="asha@example.com", message="hello") # all named, any order
send("hello", "asha@example.com", 5, True) # all positionalPositional arguments are matched by order. Named ones are matched by name and can appear in any order, but every positional argument must come before every named one, or you get SyntaxError: positional argument follows keyword argument.
That last call, send("hello", "asha@example.com", 5, True), is legal and bad. Six months later nobody knows what 5 and True mean. Passing booleans and bare numbers by name — retries=5, urgent=True — costs eight characters and removes a whole category of misreading.
Defaults are evaluated once, when the function is defined
This is the most famous trap in Python, and it is worth meeting deliberately:
def add_item(item, basket=[]):
basket.append(item)
return basket
add_item("rice") # ['rice']
add_item("dal") # ['rice', 'dal'] — not what anyone expectedThe empty list in the def line was created once, when Python executed the def statement, and the same list is reused on every call that does not supply one. Items accumulate across calls forever.
The mechanism is worth holding on to, because it explains the whole rule: a def line is executed like any other statement, and the default expressions in it are evaluated at that moment, not at call time.
The fix is always the same:
def add_item(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basketImmutable defaults — numbers, strings, True, None, tuples — cannot be modified, so they are safe. Never use a list, dict or set as a default value.
The same trap has a subtler form: def log(msg, when=datetime.now()) stamps every message with the time the module was imported.
*args and **kwargs
To accept any number of positional arguments, put a star in front of a name:
def total(*amounts):
return sum(amounts)
total(10, 20, 30) # amounts is the tuple (10, 20, 30)Two stars collects any named arguments into a dictionary:
def make_request(url, **options):
print(url, options)
make_request("/chat", timeout=30, stream=True)
# /chat {'timeout': 30, 'stream': True}The names args and kwargs are convention only; the stars do the work. You will meet them constantly in library code and in wrappers that pass arguments through to something else:
def logged_call(func, *args, **kwargs):
print(f"calling {func.__name__}")
return func(*args, **kwargs)A star in a call does the reverse: it unpacks. func(*[1, 2]) calls func(1, 2), and func(**{"a": 1}) calls func(a=1). Same symbol, opposite direction, decided by whether you are defining or calling.
Forcing arguments to be named
Anything after a bare * in the parameter list can only be passed by name:
def resize(image, *, width, height):
...
resize(img, width=800, height=600) # fine
resize(img, 800, 600) # TypeErrorThis is how a library stops you from silently swapping two same-typed arguments, and it is worth doing in your own code for any function with more than about three parameters. It also lets you reorder or add parameters later without breaking callers.
The mirror image is /, which forces the parameters before it to be positional. You mostly meet it in the standard library's documentation rather than writing it yourself.
Arguments are passed by object reference
Python does not copy what you pass in and does not hand over a pointer to your variable. The function receives the same object the caller had:
def spoil(items):
items.append("surprise")
basket = ["rice"]
spoil(basket)
print(basket) # ['rice', 'surprise']The function mutated the caller's list. But rebinding does not escape:
def replace(items):
items = ["new"] # rebinds the local name only
basket = ["rice"]
replace(basket)
print(basket) # ['rice']The one rule that covers both: mutating the object is visible to the caller; assigning a new object to the parameter name is not. Half of all confusion about Python's argument passing dissolves once that sentence is in place.
If a function must not modify what it is given, copy at the top of it, and say so in the docstring. If it is meant to modify in place, return None, following the convention that mutating operations do not hand back a value.
The one thing to keep
Default values are created once when the `def` line runs, so a mutable default is shared by every call, and a function can mutate the object you passed but cannot rebind your name.
Before you move on
A caching helper is written as `def remember(key, store={}): store[key] = time.time(); return store`. It is called from two unrelated modules and each reports seeing the other's keys. What is the mechanism?
Pick the one you would defend. Nobody sees your answer.