5.2 Advanced Parameters — Defaults, Keyword Arguments, *args, **kwargs
Python's function parameter system is highly flexible. You can pass arguments as positional, keyword, variable-length, or with defaults. Mastering this lets you design flexible and powerful APIs.
Positional Arguments
# Basic: pass values in the order they are defined
def describe_point(x: float, y: float, label: str) -> str:
return f"Point {label}: ({x}, {y})"
# Positional arguments
print(describe_point(3.0, 4.0, "A")) # Point A: (3.0, 4.0)
# Different order, different result
print(describe_point(4.0, 3.0, "B")) # Point B: (4.0, 3.0)
# Positional → keyword (improved readability)
print(describe_point(x=3.0, y=4.0, label="A"))
print(describe_point(label="C", x=1.0, y=2.0))
Default Parameters
# Parameters with defaults must come after those without
def connect(host: str, port: int = 80, timeout: float = 30.0) -> str:
return f"Connect: {host}:{port} (timeout={timeout}s)"
print(connect("example.com")) # Uses port=80, timeout=30.0
print(connect("example.com", 443)) # port=443, timeout=30.0
print(connect("example.com", 443, 10.0)) # All specified
# Warning: using a mutable object as a default value is a bug!
# Bad
def append_bad(item, lst=[]): # lst is created ONCE at function definition
lst.append(item)
return lst
print(append_bad(1)) # [1]
print(append_bad(2)) # [1, 2] — previous list is reused!
print(append_bad(3)) # [1, 2, 3]
# Correct: use None as default, create a new object inside
def append_good(item: int, lst: list[int] | None = None) -> list[int]:
if lst is None:
lst = []
lst.append(item)
return lst
print(append_good(1)) # [1]
print(append_good(2)) # [2] — fresh list every time
print(append_good(3, [10, 20])) # [10, 20, 3]
# Same caution with datetime
from datetime import datetime
def log_good(msg: str, timestamp: datetime | None = None) -> None:
if timestamp is None:
timestamp = datetime.now()
print(f"[{timestamp.strftime('%H:%M:%S')}] {msg}")
Keyword Arguments
def create_profile(
name: str,
age: int,
email: str,
city: str = "Seoul",
is_premium: bool = False
) -> dict:
return {
"name": name,
"age": age,
"email": email,
"city": city,
"is_premium": is_premium,
}
profile = create_profile(
name="Alice",
age=30,
email="alice@example.com",
is_premium=True, # city uses its default
)
print(profile)
Positional-only Parameters: / (Python 3.8+)
Parameters before / can only be passed positionally.
def power(base, exponent, /):
return base ** exponent
print(power(2, 3)) # OK: 8
# print(power(base=2, exponent=3)) # TypeError: keyword not allowed
# Built-in example: len() is also positional-only
# len(obj=["a"]) → TypeError
# Mix of positional-only, regular, and keyword-only
def complex_func(pos_only, /, normal, *, kw_only):
return f"pos={pos_only}, normal={normal}, kw={kw_only}"
print(complex_func(1, 2, kw_only=3)) # OK
print(complex_func(1, normal=2, kw_only=3)) # OK
# complex_func(pos_only=1, normal=2, kw_only=3) # TypeError!
Keyword-only Parameters: *
Parameters after * must be passed as keyword arguments.
def send_email(to: str, subject: str, *, body: str, html: bool = False) -> None:
format_str = "HTML" if html else "text"
print(f"To: {to}, Subject: {subject} [{format_str}]")
print(f"Body: {body[:50]}...")
# body must be a keyword argument
send_email("alice@ex.com", "Hello", body="Hi there!", html=False)
# send_email("alice@ex.com", "Subject", "body") # TypeError
# Keyword-only to prevent accidents
def delete_records(table: str, *, confirm: bool) -> str:
"""Cannot run without confirm=True"""
if not confirm:
return "Cancelled"
return f"{table} table deleted"
result = delete_records("users", confirm=True)
print(result)
*args: Variable Positional Arguments
# *args: collects extra positional arguments as a tuple
def sum_all(*args: int | float) -> float:
return sum(args)
print(sum_all(1, 2, 3)) # 6
print(sum_all(1, 2, 3, 4, 5)) # 15
print(sum_all()) # 0
def log_messages(level: str, *messages: str) -> None:
for msg in messages:
print(f"[{level}] {msg}")
log_messages("INFO", "Server started", "Port 8080 bound", "Ready")
# args is a tuple
def inspect_args(*args):
print(f"Type: {type(args)}, Value: {args}")
inspect_args(1, "hello", True)
# Type: <class 'tuple'>, Value: (1, 'hello', True)
# Unpack an existing list with *
numbers = [3, 1, 4, 1, 5, 9]
print(sum_all(*numbers)) # 23
**kwargs: Variable Keyword Arguments
def create_html_tag(tag: str, content: str, **attrs) -> str:
attr_str = " ".join(f'{k}="{v}"' for k, v in attrs.items())
if attr_str:
return f"<{tag} {attr_str}>{content}</{tag}>"
return f"<{tag}>{content}</{tag}>"
print(create_html_tag("a", "Click", href="https://example.com", target="_blank"))
# <a href="https://example.com" target="_blank">Click</a>
print(create_html_tag("p", "Hello"))
# <p>Hello</p>
# kwargs is a dictionary
def inspect_kwargs(**kwargs):
print(f"Type: {type(kwargs)}")
for key, value in kwargs.items():
print(f" {key} = {value}")
inspect_kwargs(name="Alice", age=30, city="Seoul")
# Unpack a dict with **
options = {"color": "red", "size": "large", "bold": "true"}
print(create_html_tag("span", "Text", **options))
Parameter Order Rules
# Order: positional-only / regular / *args / keyword-only / **kwargs
def full_example(
pos_only, # Positional-only (before /)
/,
regular, # Regular (positional or keyword)
*args, # Variable positional
keyword_only, # Keyword-only (after *)
**kwargs # Variable keyword
) -> dict:
return {
"pos_only": pos_only,
"regular": regular,
"args": args,
"keyword_only": keyword_only,
"kwargs": kwargs,
}
result = full_example(
1, # pos_only
2, # regular
3, 4, 5, # *args
keyword_only="kw", # keyword_only
extra1="a", # **kwargs
extra2="b"
)
print(result)
# {'pos_only': 1, 'regular': 2, 'args': (3, 4, 5),
# 'keyword_only': 'kw', 'kwargs': {'extra1': 'a', 'extra2': 'b'}}
Real-world Example: Flexible API Design
from typing import Any
class QueryBuilder:
"""SQL query builder (simulation)"""
def __init__(self, table: str):
self.table = table
self._conditions: list[str] = []
self._order: str | None = None
self._limit: int | None = None
def where(self, **conditions: Any) -> "QueryBuilder":
"""Add WHERE conditions — pass as keyword arguments"""
for col, val in conditions.items():
self._conditions.append(f"{col} = '{val}'")
return self
def order_by(self, column: str, *, desc: bool = False) -> "QueryBuilder":
"""ORDER BY clause — desc is keyword-only"""
direction = "DESC" if desc else "ASC"
self._order = f"ORDER BY {column} {direction}"
return self
def limit(self, n: int, /) -> "QueryBuilder":
"""LIMIT clause — n is positional-only"""
self._limit = n
return self
def build(self) -> str:
query = f"SELECT * FROM {self.table}"
if self._conditions:
query += " WHERE " + " AND ".join(self._conditions)
if self._order:
query += " " + self._order
if self._limit:
query += f" LIMIT {self._limit}"
return query
query = (
QueryBuilder("users")
.where(city="Seoul", is_active=True)
.order_by("created_at", desc=True)
.limit(10)
.build()
)
print(query)
# SELECT * FROM users WHERE city = 'Seoul' AND is_active = 'True'
# ORDER BY created_at DESC LIMIT 10
Pro Tips
1. Enforce Keyword-only Without Variable Args
def configure(*, host: str, port: int, debug: bool = False) -> dict:
return {"host": host, "port": port, "debug": debug}
# configure("localhost", 8080) # TypeError
config = configure(host="localhost", port=8080, debug=True)
print(config)
2. Inspect Function Signatures
import inspect
def example(a: int, b: str = "hello", *args, kw_only: bool = False, **kwargs):
pass
sig = inspect.signature(example)
for name, param in sig.parameters.items():
kind = param.kind.name
default = param.default if param.default != inspect.Parameter.empty else "none"
annotation = param.annotation if param.annotation != inspect.Parameter.empty else "none"
print(f" {name}: kind={kind}, default={default}, type={annotation}")
3. Transparent Argument Forwarding
import time
import functools
def timing_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs) # Forward all args transparently
elapsed = time.perf_counter() - start
print(f"{func.__name__} execution time: {elapsed:.4f}s")
return result
return wrapper
@timing_decorator
def slow_function(n: int, multiplier: int = 1) -> int:
return sum(range(n)) * multiplier
result = slow_function(1_000_000, multiplier=2)
print(f"Result: {result}")