Parameters, Keyword Arguments, and Defaults
Parameters, Keyword Arguments, and Defaults
After reading this chapter, you will master positional vs keyword arguments, configure safe default parameters, and use modern positional-only (/) and keyword-only (*) boundary markers.
Mental model
Python provides precise control over how arguments are passed into a function signature:
def api_request(method, url, /, timeout=30, *, secure=True, headers=None):
└───────────┘ └────────┘ └────────────────────────┘
Positional-Only Standard Keyword-Only
(before "/") (positional or kw) (after "*")
/(Positional-only boundary): Parameters before/must be passed by position; callers cannot pass them asurl="...".*(Keyword-only boundary): Parameters after*must be passed by name (secure=False); prevents bugs where callers pass confusing boolean flags positionally.- Default arguments: Evaluated once when the function is defined, not every time it is called.
Minimal example
Save as parameter_boundaries.py:
# parameter_boundaries.py
def query_endpoint(
service: str, # Positional-only parameter (before '/')
/,
retries: int = 3, # Standard parameter
*,
use_tls: bool = True, # Keyword-only parameter (after '*')
timeout: float = 5.0, # Keyword-only parameter
) -> None:
print(f"Service: {service} | Retries: {retries} | TLS: {use_tls} | Timeout: {timeout}s")
def main() -> None:
# 1. Valid invocation
query_endpoint("auth_svc", 5, use_tls=True, timeout=10.0)
# 2. Valid invocation relying on defaults
query_endpoint("billing_svc", use_tls=False)
# 3. Invalid: query_endpoint(service="auth_svc") -> TypeError: positional-only argument
# 4. Invalid: query_endpoint("auth_svc", 3, True) -> TypeError: takes 2 positional arguments
if __name__ == "__main__":
main()Run via uv run python parameter_boundaries.py:
Service: auth_svc | Retries: 5 | TLS: True | Timeout: 10.0s
Service: billing_svc | Retries: 3 | TLS: False | Timeout: 5.0s
Worked examples
Case 1: Preventing the “Boolean Parameter Trap”
When functions take boolean flags positionally, call sites become unreadable (render(True, False, True)). Keyword-only arguments force clarity:
# keyword_only_api.py
def configure_cache(
host: str,
port: int,
*,
cluster_mode: bool = False,
ssl_enabled: bool = True,
eviction_policy: str = "lru"
) -> None:
print(f"Cache on {host}:{port} [cluster={cluster_mode}, ssl={ssl_enabled}, policy={eviction_policy}]")
if __name__ == "__main__":
# Call site is self-documenting
configure_cache("redis.internal", 6379, cluster_mode=True, ssl_enabled=True)Run:
uv run python keyword_only_api.pyCase 2: The Mutable Default Argument Trap and the Fix
Because default values are evaluated at definition time, a mutable default (like [] or {}) is shared across all calls:
# mutable_default_fix.py
from datetime import datetime
# THE BUG:
def add_log_broken(msg: str, tags: list[str] = []) -> list[str]:
tags.append(msg)
return tags
# THE FIX: Use None as default, allocate inside function
def add_log_safe(msg: str, tags: list[str] | None = None) -> list[str]:
if tags is None:
tags = []
tags.append(msg)
return tags
if __name__ == "__main__":
print("Broken with mutable default:")
print("Call 1:", add_log_broken("error 1"))
print("Call 2:", add_log_broken("error 2")) # Re-uses the same list!
print("\nSafe with None sentinel:")
print("Call 1:", add_log_safe("error 1"))
print("Call 2:", add_log_safe("error 2")) # Fresh list allocatedRun:
uv run python mutable_default_fix.pyOutput:
Broken with mutable default:
Call 1: ['error 1']
Call 2: ['error 1', 'error 2']
Safe with None sentinel:
Call 1: ['error 1']
Call 2: ['error 2']
Pitfalls
Pitfall 1: Evaluating dynamic defaults like datetime.now() in the signature
# Danger: The timestamp is frozen to when the module was loaded!
def record_event(name: str, timestamp: datetime = datetime.now()):
pass
# Fix:
def record_event(name: str, timestamp: datetime | None = None):
actual_time = timestamp if timestamp is not None else datetime.now()Exercises
- Define a function
format_currency(amount, /, *, currency="USD", symbol=True)and test calling it with valid and invalid argument permutations. - Write a function
append_item(item, collection=None)that safely handles list mutation and returns the updated list. - Use
inspect.signaturefrom the standard library to inspect the parameters and default values of a custom function.
Further reading
- PEP 570: Python Positional-Only Parameters.
- PEP 3102: Keyword-Only Arguments.
- Python Standard Library:
inspect.signature.