- Parameter Kinds in Python: Positional vs Keyword Enforcement
- Positional-Only Parameters with /
- Keyword-Only Parameters with *
- Combining Both: The Complete Signature Blueprint
Parameter Kinds in Python: Positional vs Keyword Enforcement
– Standard Python parameter behavior:
By default, parameters in Python can be passed either by position (func(10)) or by keyword (func(x=10)).
While flexible, this default behavior can lead to ambiguous call sites or prevent library authors from renaming internal arguments without breaking public APIs.
– The role of / and * markers:
– / (Slash): Enforces positional-only arguments for all parameters defined before it.
– * (Asterisk): Enforces keyword-only arguments for all parameters defined after it.
– Parameters placed between / and * can be passed using either style.
Positional-Only Parameters with /
– Why use positional-only arguments (PEP 570):
Introduced in Python 3.8, the / marker prevents callers from relying on parameter names.
Use cases include:
– When parameter names have no semantic value (e.g., mathematical inputs like x, y).
– When you want the freedom to rename parameters in future releases without breaking user code.
– Matching the behavior of built-in C functions (like abs() or len()).
– Example: Enforcing positional arguments:
# Everything before '/' MUST be passed by position def calculate_tax(amount: float, rate: float, /) -> float: return amount * rate # 1. Valid call: tax = calculate_tax(100.0, 0.20) # 2. Invalid call (raises TypeError: calculate_tax() got some positional-only arguments passed as keyword arguments): # tax = calculate_tax(amount=100.0, rate=0.20) |
Keyword-Only Parameters with *
– Why use keyword-only arguments (PEP 3102):
A bare * forces callers to explicitly name all parameters that follow.
Use cases include:
– Boolean flags: Calling create_user("Alice", True, False) is unreadable, whereas create_user("Alice", is_admin=True, notify=False) is explicit and self-documenting.
– Preventing silent bugs caused by passing arguments in the wrong order.
– Example: Enforcing explicit keyword arguments:
# Everything after '*' MUST be explicitly named at the call site def create_button(label: str, *, is_primary: bool = False, disabled: bool = False) -> dict: return {"label": label, "is_primary": is_primary, "disabled": disabled} # 1. Valid call: btn = create_button("Submit", is_primary=True) # 2. Invalid call (raises TypeError: create_button() takes 1 positional argument but 2 were given): # btn = create_button("Submit", True) |
Combining Both: The Complete Signature Blueprint
– The full anatomy of a Python signature:
You can combine both markers in the same function to strictly control each argument segment:
def func(pos_only, /, standard, *, kw_only): ...
– Real-world example: Data export function:
from typing import Any def export_data( data: list[dict[str, Any]], # Positional-only (obvious input payload) /, target_format: str = "json", # Standard (can be passed positionally or by name) *, compress: bool = False, # Keyword-only (boolean flag) overwrite: bool = True # Keyword-only (boolean flag) ) -> str: return f"Exported {len(data)} items to {target_format} (compressed={compress})" # --- Valid calls --- # 1. Standard call using positional payload and keyword options: export_data([{"id": 1}], "csv", compress=True) # 2. Explicitly naming the format parameter: export_data([{"id": 1}], target_format="xml", overwrite=False) # --- Invalid calls --- # 1. Passing positional-only 'data' with a keyword (TypeError): # export_data(data=[{"id": 1}], target_format="json") # 2. Passing keyword-only 'compress' positionally (TypeError): # export_data([{"id": 1}], "json", True) |