Default and Keyword Arguments in Python: Flexible APIs and the Mutable Default Trap
Well-designed Python functions balance flexibility (callers can customise behaviour) with convenience (callers do not have to specify everything every time). Default arguments and keyword arguments are the two main tools for achieving this balance. They also carry one of the most notorious bugs in Python โ the mutable default argument โ that trips up even experienced developers.
Default Arguments
A default argument provides a fallback value when the caller does not supply one.
def connect(host, port=5432, timeout=30, ssl=True): print(f"Connecting to {host}:{port} (timeout={timeout}s, ssl={ssl})")
# Most callers only need to specify the hostconnect("db.example.com")# Connecting to db.example.com:5432 (timeout=30s, ssl=True)
# Override what you needconnect("db.example.com", port=3306, ssl=False)# Connecting to db.example.com:3306 (timeout=30s, ssl=False)Default values are evaluated once when the function is defined, not each time the function is called. For immutable values like integers, strings, tuples, and None, this is fine. For mutable values, it causes problems.
The Mutable Default Argument Bug
This is one of the most frequently encountered Python gotchas:
def add_item(item, collection=[]): # Bug: list created once collection.append(item) return collection
print(add_item("apple")) # ['apple']print(add_item("banana")) # ['apple', 'banana'] โ wrong! should be ['banana']print(add_item("cherry")) # ['apple', 'banana', 'cherry'] โ accumulatingThe same list object is used across all calls because it was created when the function was defined. Every call that does not pass collection modifies the same list.
The standard fix is to use None as the default and create the mutable object inside the function:
def add_item(item, collection=None): if collection is None: collection = [] # new list on each call collection.append(item) return collection
print(add_item("apple")) # ['apple']print(add_item("banana")) # ['banana'] โ correctprint(add_item("cherry")) # ['cherry'] โ correct
# Can still pass an existing collectionmy_list = ["existing"]print(add_item("new", my_list)) # ['existing', 'new']This applies to any mutable default: lists, dicts, sets, and custom mutable objects. Use None for all of them.
Keyword Arguments: Passing by Name
Any parameter can be passed by name (as a keyword argument), regardless of its position in the function signature.
def create_user(username, email, role="reader", active=True): print(f"Creating user: {username}, {email}, role={role}, active={active}")
# Positionalcreate_user("alice", "alice@example.com", "admin", True)
# Keyword โ order doesn't mattercreate_user(email="bob@example.com", username="bob", active=False)
# Mix: positional first, then keywordcreate_user("carol", "carol@example.com", active=False)Keyword arguments make call sites self-documenting. When a function has several boolean or similar parameters, seeing active=False, notify=True is clearer than seeing False, True and having to count positions.
Keyword-Only Parameters
Parameters that appear after *args (or after a bare *) can only be passed by keyword โ callers cannot use positional syntax for them.
def send_message(recipient, message, *, priority="normal", encrypt=False): # priority and encrypt are keyword-only print(f"To: {recipient}") print(f"Priority: {priority}, Encrypted: {encrypt}") print(f"Message: {message}")
send_message("alice", "Hello!", priority="high", encrypt=True)
# This raises TypeError โ priority can't be positional after *# send_message("alice", "Hello!", "high")Keyword-only parameters are useful when a function has arguments that change semantics and you want callers to name them explicitly. They prevent mistakes like accidentally passing a priority where a boolean was expected.
A bare * (with no name) can force keyword-only without collecting extra positional arguments:
def resize(image, *, width, height, keep_aspect=True): # width and height are required keyword-only โ no default pass
resize(img, width=800, height=600)# resize(img, 800, 600) # TypeErrorPositional-Only Parameters (Python 3.8+)
Python 3.8 added the / separator to mark parameters as positional-only โ callers cannot pass them by keyword name.
def distance(x1, y1, x2, y2, /): # All four are positional-only return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
print(distance(0, 0, 3, 4)) # 5.0
# distance(x1=0, y1=0, x2=3, y2=4) # TypeErrorPositional-only parameters are useful in library APIs where the parameter names are implementation details you do not want to commit to.
The Full Parameter Order
A function signature can use all these features, in a specific order:
def full_example(pos_only, /, standard, *args, kw_only, **kwargs): passOrder:
- Positional-only parameters (before
/) - Standard parameters
*args- Keyword-only parameters (after
*or*args) **kwargs
In practice, most functions use a small subset of these. The common patterns are:
def simple(a, b, c=10): # positional + defaultdef flexible(a, *args, **kwargs): # collect extrasdef strict(a, *, b, c=10): # keyword-onlyDefault Values and Mutability: A Summary
| Default type | Safe? | Example |
|---|---|---|
None | Yes | def f(x=None) |
| Integer | Yes | def f(n=0) |
| String | Yes | def f(s="") |
| Tuple | Yes | def f(t=()) |
| List | No | def f(lst=[]) โ use None instead |
| Dict | No | def f(d={}) โ use None instead |
| Custom object | Depends | Risky if mutable |
Common Mistakes
Putting keyword-only parameters before *args. Parameters before *args are positional. Only parameters after *args (or after a bare *) are keyword-only.
Providing defaults for keyword-only parameters that must be required. If callers must provide a value, do not give it a default:
def process(data, *, output_format): # required keyword-only โ no default pass
process([1, 2, 3], output_format="csv")# process([1, 2, 3]) # TypeError: missing keyword argument 'output_format'Ordering non-default arguments after default arguments. This is a SyntaxError:
def bad(a=10, b): # SyntaxError โ b has no default but comes after one passNon-default parameters must come before parameters with defaults.