Chapter 7 — Arguments, Parameters, and API Design
1. Opening problem
def connect(
host,
port,
timeout,
secure,
):
...
Call:
connect(
"db.internal",
5432,
5,
True,
)
What does 5 represent? What does True represent?
Python's parameter system lets us design much clearer APIs:
- positional arguments;
- keyword arguments;
- positional-only parameters;
- keyword-only parameters;
- defaults;
*args;**kwargs;- unpacking;
- typed forwarding.
2. Parameters and arguments
A parameter appears in a definition:
def greet(name):
...
An argument appears in a call:
greet("Steve")
This distinction is useful in documentation and debugging.
3. Positional arguments
def subtract(
left: int,
right: int,
) -> int:
return left - right
subtract(10, 3)
Position works when meaning is obvious.
This is less clear:
create_user(
"Steve",
True,
False,
30,
)
Use keyword arguments when the values are not self-explanatory.
4. Keyword arguments
create_user(
name="Steve",
active=True,
verified=False,
trial_days=30,
)
Benefits:
- readable calls;
- fewer ordering mistakes;
- easier code review;
- safer boolean values;
- easier future extension.
A common style is to keep one or two obvious primary values positional and make configuration named.
5. Defaults
def connect(
host: str,
port: int = 5432,
) -> None:
...
Defaults are evaluated when the function definition executes.
Immutable defaults are generally safe:
def configure(
timeout: float = 5.0,
secure: bool = True,
) -> None:
...
Mutable defaults are dangerous:
def add_tag(
tag: str,
tags: list[str] = [],
):
...
Use None and decide whether caller-owned input should be copied:
def add_tag(
tag: str,
tags: list[str] | None = None,
) -> list[str]:
result = (
[]
if tags is None
else list(tags)
)
result.append(tag)
return result
6. Keyword-only parameters
A * makes later parameters keyword-only:
def connect(
host: str,
port: int,
*,
timeout: float = 5.0,
secure: bool = True,
) -> None:
...
Valid:
connect(
"db.internal",
5432,
timeout=10.0,
secure=True,
)
Invalid:
connect(
"db.internal",
5432,
10.0,
True,
)
Keyword-only parameters fit:
- booleans;
- units;
- optional settings;
- behaviour flags;
- several arguments of the same type;
- values likely to be extended later.
7. Positional-only parameters
A / makes earlier parameters positional-only:
def ratio(
numerator: float,
denominator: float,
/,
) -> float:
return numerator / denominator
Valid:
ratio(10, 2)
Invalid:
ratio(
numerator=10,
denominator=2,
)
Reasons:
- names are implementation details;
- names may change without breaking callers;
- operation is naturally positional;
- API should resemble built-ins;
- wrappers may need to accept matching keyword names.
8. Combined signatures
def request(
method: str,
url: str,
/,
body: bytes | None = None,
*,
timeout: float = 5.0,
follow_redirects: bool = True,
) -> bytes:
...
Interpretation:
method,url: positional-only;body: positional or keyword;timeout,follow_redirects: keyword-only.
A signature communicates intended usage.
9. *args
def total(
*values: float,
) -> float:
return sum(values)
Inside, values is a tuple.
Use *args when any number of homogeneous positional values is natural.
Avoid:
def create_user(*args):
...
The contract becomes unclear.
10. **kwargs
def log_event(
event: str,
**metadata: object,
) -> None:
print(event, metadata)
Usage:
log_event(
"user_created",
user_id=42,
source="api",
)
Good uses:
- metadata;
- forwarding;
- wrappers;
- adapter layers;
- naturally extensible optional data.
Risks:
- hidden contracts;
- misspelled names;
- weak static typing;
- unsupported values accepted silently.
Do not use **kwargs merely to avoid designing a signature.
11. Unpacking
coordinates = (10, 20)
move(*coordinates)
options = {
"timeout": 10.0,
"secure": False,
}
connect(
"db.internal",
5432,
**options,
)
Unpacking is useful for trusted structured data.
Dangerous:
create_user(**request_payload)
An untrusted payload can include:
- missing fields;
- extra fields;
- wrong types;
- security-sensitive fields;
- values that bypass domain validation.
Parse and validate first.
12. Binding errors
Python reports:
Missing argument
greet()
Too many positional arguments
greet("Steve", "extra")
Multiple values
connect(
"db",
5432,
host="other",
)
Unexpected keyword
connect(
"db",
5432,
unsupported=True,
)
These rules become especially important when decorators forward arguments.
13. Typed forwarding with ParamSpec
from collections.abc import Callable
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def invoke(
function: Callable[P, R],
*args: P.args,
**kwargs: P.kwargs,
) -> R:
return function(
*args,
**kwargs,
)
ParamSpec preserves the callable's full parameter structure.
Without it, wrappers often degrade type checking to Any.
14. Boolean flags
Unreadable:
export_report(
data,
True,
False,
True,
)
Better:
export_report(
data,
include_headers=True,
compress=False,
overwrite=True,
)
When a boolean really represents a mode, an enum can be clearer:
from enum import Enum
class Compression(Enum):
NONE = "none"
GZIP = "gzip"
export_report(
data,
compression=Compression.GZIP,
)
Several booleans can permit invalid combinations. A richer type can prevent them.
15. Flags that change return types
Problematic:
def load_user(
user_id: int,
as_dict: bool = False,
):
...
The return type changes according to a flag.
Prefer separate contracts:
def load_user(
user_id: int,
) -> User:
...
def load_user_record(
user_id: int,
) -> dict[str, object]:
...
Or convert at a boundary. One function should ideally have one stable semantic identity.
16. Configuration objects
When related options grow, group them:
from dataclasses import dataclass
@dataclass(frozen=True)
class RetryConfig:
max_attempts: int = 3
initial_delay_seconds: float = 0.5
backoff_multiplier: float = 2.0
def __post_init__(self) -> None:
if self.max_attempts < 1:
raise ValueError(
"max_attempts must be positive"
)
if self.initial_delay_seconds < 0:
raise ValueError(
"delay must not be negative"
)
if self.backoff_multiplier < 1:
raise ValueError(
"backoff must be at least 1"
)
Benefits:
- related validation in one place;
- reusable configuration;
- reduced signature size;
- clearer domain concepts;
- easier evolution.
Do not introduce a configuration class unless it adds real meaning.
17. TypeScript comparison
TypeScript commonly uses an options object:
function connect(
host: string,
port: number,
options: {
timeout?: number;
secure?: boolean;
} = {}
): void {}
Python can often express the same idea with keyword-only parameters:
def connect(
host: str,
port: int,
*,
timeout: float = 5.0,
secure: bool = True,
) -> None:
...
For complex groups, Python can also use dataclasses or typed dictionaries.
18. Common mistakes
- too many positional values;
- mutable defaults;
- blind request unpacking;
**kwargshiding the real API;- boolean flags creating invalid combinations;
- wrappers losing type information;
- functions returning unrelated types based on flags;
- configuration objects created without benefit.
19. English vocabulary
| Term | Meaning |
|---|---|
| parameter | input name in a function definition |
| argument | value supplied in a call |
| positional | bound by order |
| keyword | bound by name |
| variadic | accepting a variable number of inputs |
| unpacking | expanding a collection into arguments |
| forwarding | passing arguments to another callable |
| signature | declared parameter structure |
| compatibility | ability of old callers to keep working |
| self-documenting | understandable from the code itself |
Useful sentences:
- “These booleans should be keyword-only.”
- “Blind unpacking exposes the constructor contract.”
- “The wrapper preserves the original parameter specification.”
- “A configuration object groups and validates related settings.”
- “The public API should not depend on these internal names.”
20. Speaking task
Explain for seven minutes:
How Python signatures help create readable and stable APIs.
21. Writing task
Review this design:
create_report(
data,
True,
False,
20,
"pdf",
None,
)
Write a 300-word redesign proposal.
22. Exercises
Exercise 1
Redesign:
def create_account(
name,
active,
admin,
send_email,
trial_days,
):
...
Exercise 2
Create positional-only percentage(part, whole, /).
Exercise 3
Create keyword-only download options and validate timeout and retry count.
Exercise 4
Implement typed call_and_log forwarding.
Exercise 5
Create an immutable, validated PaginationConfig.
23. Complete solutions
Solution 1
def create_account(
name: str,
*,
active: bool = True,
admin: bool = False,
send_email: bool = True,
trial_days: int = 30,
) -> None:
if trial_days < 0:
raise ValueError(
"trial_days must not be negative"
)
Solution 2
def percentage(
part: float,
whole: float,
/,
) -> float:
if whole == 0:
raise ValueError(
"whole must not be zero"
)
return part / whole * 100
Solution 3
def download(
url: str,
*,
timeout: float = 5.0,
retries: int = 3,
) -> bytes:
if timeout <= 0:
raise ValueError(
"timeout must be positive"
)
if retries < 0:
raise ValueError(
"retries must not be negative"
)
return b""
Solution 4
from collections.abc import Callable
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def call_and_log(
function: Callable[P, R],
*args: P.args,
**kwargs: P.kwargs,
) -> R:
print(
f"Calling {function.__name__}"
)
result = function(
*args,
**kwargs,
)
print(
f"Finished {function.__name__}"
)
return result
Solution 5
from dataclasses import dataclass
@dataclass(frozen=True)
class PaginationConfig:
page_size: int = 20
maximum_page_size: int = 100
default_page_number: int = 1
def __post_init__(self) -> None:
if self.page_size <= 0:
raise ValueError(
"page_size must be positive"
)
if self.maximum_page_size <= 0:
raise ValueError(
"maximum_page_size must be positive"
)
if (
self.page_size
> self.maximum_page_size
):
raise ValueError(
"page_size exceeds maximum"
)
if self.default_page_number <= 0:
raise ValueError(
"default page must be positive"
)
24. Chapter checkpoint
You should now be able to explain:
- parameters versus arguments;
- positional and keyword binding;
/and*;- safe default values;
- justified uses of
*argsand**kwargs; - typed forwarding;
- configuration objects;
- stable API evolution.