Parameters, Keyword Arguments, and Defaults

Updated

September 7, 2026

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 as url="...".
  • * (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.py

Case 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 allocated

Run:

uv run python mutable_default_fix.py

Output:

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

  1. Define a function format_currency(amount, /, *, currency="USD", symbol=True) and test calling it with valid and invalid argument permutations.
  2. Write a function append_item(item, collection=None) that safely handles list mutation and returns the updated list.
  3. Use inspect.signature from 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.