Parameters in depth
Use default values, *args, **kwargs, keyword-only and positional-only parameters, and avoid the mutable-default trap.
What you will learn
- Give parameters default values and explain why a list default is dangerous
- Collect extra arguments with *args and **kwargs, and unpack with * and **
- Force callers to use keywords with a bare * and positional arguments with /
On this page
The simplest function takes a fixed number of arguments in a fixed order. Real functions need more: optional settings, any number of inputs, options that are clearer when named. Python has a small set of parameter kinds for each of these, and this lesson goes through all of them.
Defaults and keyword arguments
A parameter with =value is optional. Callers can pass arguments by position, by name, or leave defaults alone.
def power(base, exponent=2): return base**exponent
print(power(5))print(power(2, 10))print(power(exponent=3, base=2))2510248power(exponent=3, base=2) passes keyword arguments: order no longer matters because each value is labelled. Parameters with defaults must come after those without.
The next function is the most famous trap in Python. The default value [] is created once, when def runs, and stored on the function. Every call that omits items shares that one list, so the second call “remembers” 'a'.
def add_item_bad(item, items=[]): items.append(item) return items
print(add_item_bad("a"))print(add_item_bad("b"))print(add_item_bad.__defaults__)['a']['a', 'b'](['a', 'b'],)The __defaults__ line shows the shared list stored on the function. Defaults that are mutable (lists, dicts, sets) behave this way.
The fix is the None idiom: default to None, and create a fresh list inside the body on each call. Numbers, strings, tuples and None are immutable, so they are safe as defaults. Test with is None rather than truthiness, so a caller’s empty list is still used.
def add_item(item, items=None): if items is None: items = [] items.append(item) return items
print(add_item("a"))print(add_item("b"))print(add_item("c", ["x"]))['a']['b']['x', 'c']Any number of arguments: *args and **kwargs
A parameter written *numbers gathers extra positional arguments into a tuple. A parameter written **details gathers extra keyword arguments into a dict. The names args and kwargs are convention only; the stars do the work.
def total(*numbers): print(type(numbers).__name__, numbers) return sum(numbers)
print(total())print(total(1, 2, 3))tuple ()0tuple (1, 2, 3)6def describe(**details): print(type(details).__name__, details) for key, value in details.items(): print(f" {key} -> {value}")
describe(name="Ada", born=1815)dict {'name': 'Ada', 'born': 1815} name -> Ada born -> 1815The same stars work on the calling side, in the opposite direction. total(*values) spreads a list into separate positional arguments, and describe(**options) spreads a dict into keyword arguments.
def total(*numbers): return sum(numbers)
def describe(**details): return details
values = [4, 5, 6]print(total(*values))
options = {"name": "Grace", "born": 1906}print(describe(**options))15{'name': 'Grace', 'born': 1906}A signature can mix all three: a normal parameter, then the gathered positionals, then the gathered keywords.
def log(level, *messages, **extras): print(level, messages, extras)
log("INFO", "started", "ok", user="ada", retry=False)INFO ('started', 'ok') {'user': 'ada', 'retry': False}Keyword-only and positional-only parameters
Anything after *args (or after a bare *) can only be passed by keyword. Use that for options that read badly as bare values.
def connect(host, port=8080, *, timeout=5, secure=True): return f"{host}:{port} timeout={timeout} secure={secure}"
print(connect("example.org"))print(connect("example.org", 9000, timeout=30))example.org:8080 timeout=5 secure=Trueexample.org:9000 timeout=30 secure=Trueconnect("example.org", 9000, 30) would leave a reader guessing what 30 means, so the bare * makes it an error:
def connect(host, port=8080, *, timeout=5, secure=True): return f"{host}:{port} timeout={timeout} secure={secure}"
print(connect("example.org", 9000, 30))TypeError: connect() takes from 1 to 2 positional arguments but 3 were givenThe exact wording of these messages can differ between Python versions.
Anything before a / can only be passed by position. This syntax is available from Python 3.8 (PEP 570), so it works on every version this course supports. It suits functions whose parameter names carry no meaning, such as clamp(value, low, high, /), and lets the author rename them later without breaking callers. Many built-ins use it: len(obj, /).
def clamp(value, low, high, /): return max(low, min(value, high))
print(clamp(15, 0, 10))print(clamp(-3, 0, 10))100Naming the arguments is refused:
def clamp(value, low, high, /): return max(low, min(value, high))
print(clamp(value=15, low=0, high=10))TypeError: clamp() got some positional-only arguments passed as keyword arguments: 'value, low, high'Here is every kind in one signature, in the only order Python allows:
import inspect
def everything(a, b, /, c, d=4, *args, e, f=6, **kwargs): return (a, b, c, d, args, e, f, kwargs)
print(everything(1, 2, 3, e=5))print(everything(1, 2, 3, 40, 50, 60, e=5, f=7, z=0))print(inspect.signature(everything))(1, 2, 3, 4, (), 5, 6, {})(1, 2, 3, 40, (50, 60), 5, 7, {'z': 0})(a, b, /, c, d=4, *args, e, f=6, **kwargs)Reading left to right: positional-only (a, b), then normal (c, d), then *args, then keyword-only (e is required, f has a default), then **kwargs. You will rarely need all of them at once, but when you read a library’s signature this is the key.
Common pitfalls
- Mutable defaults.
def f(x, items=[])shares one list across calls. Default toNoneand build the list inside. - Non-default after default.
def f(a=1, b)is aSyntaxError. Required parameters come first, unless you put a*between them. - Passing the same argument twice.
power(5, base=5)raises aTypeErrorcomplaining of multiple values for the argumentbase. - Forgetting the star when calling.
total(values)passes one list, sosum(numbers)receives([4, 5, 6],)and fails. Writetotal(*values). - Modifying
kwargsor a list argument for the caller. A function that mutates a list it was handed changes the caller’s list too, as the lists lesson explained with aliasing. Copy it first if you need to change it. - Over-using
**kwargs. It silently accepts misspelled option names. Prefer named parameters unless you really are forwarding options on.
Write format_name(first, last, *, middle="", title="", last_first=False). By default it returns "Ada Lovelace". With middle it inserts the middle name, with title it prefixes it, and with last_first=True it returns "Lovelace, Ada".
Input: the five calls in the solution. Expected output:
Ada LovelaceAda Augusta LovelaceLovelace, AdaRear Admiral Grace Brewster HopperTuring, AlanShow solution
def format_name(first, last, *, middle="", title="", last_first=False): """Build a display name from its parts.""" given = f"{first} {middle}" if middle else first if last_first: name = f"{last}, {given}" else: name = f"{given} {last}" if title: name = f"{title} {name}" return name
print(format_name("Ada", "Lovelace"))print(format_name("Ada", "Lovelace", middle="Augusta"))print(format_name("Ada", "Lovelace", last_first=True))print(format_name("Grace", "Hopper", title="Rear Admiral", middle="Brewster"))print(format_name(last="Turing", first="Alan", last_first=True))Ada LovelaceAda Augusta LovelaceLovelace, AdaRear Admiral Grace Brewster HopperTuring, AlanThe bare * means the three options must be named, so format_name("Ada", "Lovelace", True) raises a TypeError and nobody has to wonder what True meant. The defaults are all immutable strings and a bool, so they are safe. first and last can still be passed by name in any order, as the last call shows.
This function is meant to return a new list each time, but the second call returns a list holding both names. Fix it.
Input: collect("Ada"), collect("Grace"), collect("Alan", ["Ada"]). Expected output: ['Ada'], ['Grace'], ['Ada', 'Alan'].
def collect(name, names=[]): names.append(name) return names
print(collect("Ada"))print(collect("Grace"))print(collect("Alan", ["Ada"]))['Ada']['Ada', 'Grace']['Ada', 'Alan']Show solution
def collect(name, names=None): if names is None: names = [] names.append(name) return names
print(collect("Ada"))print(collect("Grace"))print(collect("Alan", ["Ada"]))['Ada']['Grace']['Ada', 'Alan']The default is now None, which is immutable, and each call that omits the list creates its own.
Write average(*numbers, default=0) that returns the average of its arguments, or default when none are given. Print average(2, 4, 6), average() and average(default=-1).
Input: those three calls. Expected output: 4.0, 0 and -1.
Show solution
def average(*numbers, default=0): if not numbers: return default return sum(numbers) / len(numbers)
print(average(2, 4, 6))print(average())print(average(default=-1))4.00-1*numbers gathers the positional arguments into a tuple, which is empty when there are none. default comes after the star, so it can only be passed by name.
Summary
- Defaults are evaluated once, so use
Noneand create mutable values inside the function. *argsgathers extra positionals into a tuple and**kwargsgathers extra keywords into a dict;*and**at the call site spread them back out.- A bare
*makes later parameters keyword-only, and a/makes earlier ones positional-only (3.8 and later).
Next: scope and the LEGB rule, which explains which variable a name refers to inside a function.