How to use default and keyword arguments effectively¶
This guide covers practical patterns for writing functions with default values, keyword-only arguments, and configuration-style parameter lists. Use these patterns to create flexible, readable, and safe function interfaces.
Setting sensible default values¶
Default values let callers omit arguments that have a common or expected value. Place the most commonly overridden parameters first, and the least commonly overridden parameters last.
def connect(host: str, port: int = 5432, timeout: int = 30) -> str:
return f"Connecting to {host}:{port} with {timeout}s timeout"
print(connect("db.example.com"))
print(connect("db.example.com", port=3306))
print(connect("db.example.com", port=3306, timeout=10))
Using None as a default for mutable objects¶
Never use a mutable object such as a list or dictionary as a default value. Python evaluates default values once at function definition time, so all calls share the same object. This leads to unexpected behaviour.
# WRONG: mutable default is shared across calls
def add_item_broken(item: str, items: list[str] = []) -> list[str]:
items.append(item)
return items
result1 = add_item_broken("apple")
result2 = add_item_broken("banana")
print(f"result1: {result1}") # ['apple', 'banana'] — not what you want
print(f"result2: {result2}") # ['apple', 'banana'] — same object
# CORRECT: use None and create a new list inside the function
def add_item(item: str, items: list[str] | None = None) -> list[str]:
if items is None:
items = []
items.append(item)
return items
result1 = add_item("apple")
result2 = add_item("banana")
print(f"result1: {result1}") # ['apple']
print(f"result2: {result2}") # ['banana']
The same principle applies to dictionaries, sets, and any other mutable type.
def update_config(
key: str, value: str, config: dict[str, str] | None = None
) -> dict[str, str]:
if config is None:
config = {}
config[key] = value
return config
print(update_config("theme", "dark"))
print(update_config("language", "en"))
Keyword-only arguments with the * separator¶
Place a bare * in the parameter list to force all subsequent parameters to be passed as keyword arguments. This prevents positional mistakes and makes calls self-documenting.
def create_user(name: str, *, email: str, admin: bool = False) -> dict:
return {"name": name, "email": email, "admin": admin}
# This works — email and admin are passed as keyword arguments
user = create_user("Alice", email="alice@example.com", admin=True)
print(user)
# This raises a TypeError — you cannot pass email positionally
try:
user = create_user("Alice", "alice@example.com")
except TypeError as exc:
print(f"TypeError: {exc}")
Building configuration-style functions¶
When a function has many optional parameters, combine keyword-only arguments with sensible defaults to create a clean interface. Group related parameters together.
def send_email(
to: str,
subject: str,
body: str,
*,
cc: str | None = None,
bcc: str | None = None,
priority: str = "normal",
html: bool = False,
attachments: list[str] | None = None,
) -> str:
parts = [f"To: {to}", f"Subject: {subject}"]
if cc:
parts.append(f"CC: {cc}")
if bcc:
parts.append(f"BCC: {bcc}")
parts.append(f"Priority: {priority}")
parts.append(f"Format: {'HTML' if html else 'plain text'}")
if attachments:
parts.append(f"Attachments: {', '.join(attachments)}")
parts.append(f"Body: {body}")
return "\n".join(parts)
# Simple call — most defaults are fine
print(send_email("bob@example.com", "Hello", "Just checking in."))
print()
# Complex call — override only what you need
print(send_email(
"bob@example.com",
"Quarterly report",
"Please find the report attached.",
cc="manager@example.com",
priority="high",
attachments=["report.pdf", "data.xlsx"],
))
The sentinel object pattern¶
Sometimes None is a valid argument value, so you cannot use it to detect whether an argument was provided. Create a sentinel object to distinguish "not provided" from None.
_MISSING = object()
def update_field(record: dict, field: str, value: object = _MISSING) -> dict:
"""Update a field in a record. If value is not provided, delete the field."""
result = record.copy()
if value is _MISSING:
result.pop(field, None)
else:
result[field] = value # value can be None — that is intentional
return result
record = {"name": "Alice", "email": "alice@example.com", "phone": "01onal"}
# Set the phone field to None (value is explicitly None)
updated = update_field(record, "phone", None)
print(f"Set to None: {updated}")
# Delete the phone field entirely (value not provided)
deleted = update_field(record, "phone")
print(f"Deleted: {deleted}")
The sentinel pattern is useful in APIs where None carries meaning, such as database operations or configuration merging.
Combining positional-only and keyword-only parameters¶
Python 3.8 introduced the / separator for positional-only parameters. You can combine both separators to create precise interfaces.
def search(query: str, /, *, max_results: int = 10, case_sensitive: bool = False) -> str:
"""Search for a query string.
'query' must be positional. Options must be keyword arguments.
"""
return (
f"Searching for '{query}' "
f"(max {max_results}, case_sensitive={case_sensitive})"
)
print(search("python functions"))
print(search("python functions", max_results=5, case_sensitive=True))
Summary¶
The key patterns covered in this guide are as follows:
- Place commonly overridden parameters first, with sensible defaults for the rest
- Always use
Noneas the default for mutable parameters such as lists and dictionaries - Use the
*separator to enforce keyword-only arguments for clarity and safety - Use the
/separator when you want to reserve parameter names for callers - Use a sentinel object when
Noneis a valid argument value and you need to detect missing arguments