Zum Hauptinhalt springen

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;
  • **kwargs hiding 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

TermMeaning
parameterinput name in a function definition
argumentvalue supplied in a call
positionalbound by order
keywordbound by name
variadicaccepting a variable number of inputs
unpackingexpanding a collection into arguments
forwardingpassing arguments to another callable
signaturedeclared parameter structure
compatibilityability of old callers to keep working
self-documentingunderstandable 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:

  1. parameters versus arguments;
  2. positional and keyword binding;
  3. / and *;
  4. safe default values;
  5. justified uses of *args and **kwargs;
  6. typed forwarding;
  7. configuration objects;
  8. stable API evolution.