Default and Keyword Arguments in Python
A practical guide to Python function arguments — defaults, keyword arguments, *args, **kwargs, positional-only and keyword-only parameters, and the mutable default gotcha.
What you'll learn
- ✓How to give parameters default values
- ✓When to require keyword arguments with *
- ✓How to accept any number of arguments with *args and **kwargs
- ✓The mutable default argument pitfall and how to avoid it
- ✓How positional-only and keyword-only parameters shape an API
Prerequisites
- •Comfortable with basic functions — see Functions
Python’s function signature syntax is one of the most expressive in any mainstream language. Defaults, keyword arguments, and variadic parameters let you design APIs that are simultaneously easy for the common case and powerful for the edge cases. This post covers every form, the order they must appear in, and the one gotcha that catches almost every Python developer at least once.
Default values
A parameter can have a default value, used when the caller does not pass one:
def greet(name, greeting="Hello"):
print(f"{greeting}, {name}!")
greet("Alice") # Hello, Alice!
greet("Bob", "Hi") # Hi, Bob!
greet("Carol", greeting="Hey") # Hey, Carol!
The rule: parameters with defaults must come after parameters without defaults. This is unambiguous syntactically:
# Legal
def f(a, b, c=10): ...
# SyntaxError
def f(a, b=10, c): ...
Defaults let one function comfortably support several calling styles. They are the cleanest way to avoid writing three almost-identical helpers.
Keyword arguments
Any argument can be passed by name at the call site:
def make_request(url, method="GET", timeout=30):
print(f"{method} {url} (timeout={timeout}s)")
make_request("https://api.example.com")
make_request("https://api.example.com", method="POST")
make_request("https://api.example.com", timeout=60, method="PUT")
Once you pass a keyword argument, every later argument in the call must also be a keyword:
# Legal
make_request("...", method="POST", timeout=60)
# SyntaxError — positional after keyword
make_request("...", method="POST", 60)
Use keyword arguments liberally for clarity. Calls like connect(host, port, retries=3, timeout=10) are self-documenting in a way connect(host, port, 3, 10) is not.
The mutable default argument trap
This is the most famous Python gotcha. A default value is evaluated once, when the function is defined — not every time it is called. If the default is a mutable object, every call that omits it shares the same object:
def append_item(item, target=[]): # BAD
target.append(item)
return target
print(append_item(1)) # [1]
print(append_item(2)) # [1, 2] — !
print(append_item(3)) # [1, 2, 3] — !!
The default [] was created once, and each call mutates it. The fix is to use None as a sentinel and create the real default inside the function:
def append_item(item, target=None): # GOOD
if target is None:
target = []
target.append(item)
return target
print(append_item(1)) # [1]
print(append_item(2)) # [2]
print(append_item(3)) # [3]
The same rule applies to {} and set(). Any default that is mutable should be None in the signature.
This connects to the pass-by-assignment behaviour from Python Functions — the parameter binds to the same object the default created.
Try it yourself. Predict the output of each call:
def add(item, bag={}):
bag[item] = bag.get(item, 0) + 1
return bag
print(add("apple"))
print(add("banana"))
print(add("apple"))Then rewrite add to behave correctly without the shared state.
*args: arbitrary positional arguments
Prefix a parameter with * to collect any extra positional arguments into a tuple:
def total(*numbers):
return sum(numbers)
print(total(1, 2, 3)) # 6
print(total(1, 2, 3, 4, 5, 6)) # 21
print(total()) # 0
numbers inside the function is a plain tuple. The name args is convention, not magic; you can use whatever is descriptive.
You can mix *args with regular parameters as long as the order makes sense:
def log(level, *messages):
for m in messages:
print(f"[{level}] {m}")
log("INFO", "starting", "loaded config", "ready")
**kwargs: arbitrary keyword arguments
Prefix a parameter with ** to collect any extra keyword arguments into a dictionary:
def configure(**options):
for key, value in options.items():
print(f"{key} = {value}")
configure(host="localhost", port=8000, debug=True)
# host = localhost
# port = 8000
# debug = True
options is a plain dictionary. The most common use is forwarding arbitrary configuration to another layer — wrappers, factories, decorators.
Unpacking arguments at the call site
* and ** work in the reverse direction too: at a call site, they unpack a sequence or dictionary into positional and keyword arguments:
def add(a, b, c):
return a + b + c
values = [1, 2, 3]
print(add(*values)) # 6 — same as add(1, 2, 3)
options = {"a": 1, "b": 2, "c": 3}
print(add(**options)) # 6 — same as add(a=1, b=2, c=3)
This is how decorators and wrappers forward arguments cleanly:
def wrapper(*args, **kwargs):
print("calling")
result = real_function(*args, **kwargs)
print("done")
return result
The full parameter order
Putting it all together, a function signature follows this order:
- Positional-or-keyword parameters
*args(or a bare*separator)- Keyword-only parameters
**kwargs
def func(a, b, *args, c, d=10, **kwargs):
print(a, b, args, c, d, kwargs)
func(1, 2, 3, 4, c="cee", e="extra")
# 1 2 (3, 4) cee 10 {'e': 'extra'}
Everything after *args must be passed by keyword. This includes c in the example above — it has no default, so it is keyword-only and required.
Keyword-only parameters
If you want some parameters to be passable only by keyword — but do not need *args — use a bare * as a separator:
def open_file(path, *, mode="r", encoding="utf-8"):
print(f"Opening {path} in {mode!r} ({encoding})")
open_file("data.txt")
open_file("data.txt", mode="w")
# This fails — mode is keyword-only
# open_file("data.txt", "w")
This is the right tool for boolean flags and configuration options. It forces the caller to write verbose=True, which is far clearer at the call site than a bare True.
Positional-only parameters (Python 3.8+)
The reverse — parameters that cannot be passed by keyword — uses /:
def divide(a, b, /):
return a / b
print(divide(10, 2)) # 5.0
# divide(a=10, b=2) # TypeError
This is rarer. Reach for it when parameter names are an internal detail you do not want callers to depend on (so you can rename them later without breaking anyone).
You can combine all three groups:
def func(positional, /, normal, *, keyword_only):
...
Most functions you write will only need the middle group. The other two are tools to keep in reserve.
Try it yourself. Write a function send_email(to, *, subject, body, cc=None, bcc=None) where everything after to must be a keyword argument. Call it with various combinations and confirm that positional calls for subject raise TypeError.
A worked example: a small CSV writer
A function that prints rows in CSV format with sensible defaults and a flexible signature:
def write_csv(rows, *, delimiter=",", quote=None, header=None):
"""Print rows as CSV lines.
rows — iterable of iterables
delimiter — separator character
quote — character to wrap each field in, or None for unquoted
header — optional list printed before the rows
"""
def encode(value):
text = str(value)
if quote is None:
return text
return f"{quote}{text}{quote}"
if header is not None:
print(delimiter.join(encode(h) for h in header))
for row in rows:
print(delimiter.join(encode(v) for v in row))
rows = [
["Alice", 30, "admin"],
["Bob", 25, "user"],
]
write_csv(rows, header=["name", "age", "role"])
write_csv(rows, delimiter="|", quote='"')
Every formatting option is keyword-only, so calls remain readable no matter how many you customise. rows is positional because it is what the function is fundamentally about.
Recap
You now know:
- Defaults let one function serve many calling styles; defaults must follow non-default params
- Keyword arguments make calls self-documenting and are required after
*or*args - Mutable defaults are evaluated once — use
Noneand create the real default inside *argscollects extra positional args;**kwargscollects extra keyword args- The full parameter order is: positional,
*args, keyword-only,**kwargs - A bare
*forces what follows to be keyword-only — great for flags and options /makes earlier parameters positional-only; use it when names are internal detail
Next steps
The next post zooms out from a single function and looks at where names live: variable scope, the LEGB lookup rule, and the global and nonlocal keywords.
→ Next: Variable Scope and the global Keyword in Python
Questions or feedback? Email codeloomdevv@gmail.com.