Creating Flexible Input Handling

Real-world functions rarely take a fixed number of arguments. Users pass lists, dicts, extra options, forwarded arguments from wrappers. Python's toolkit for that is *args (a tuple of positional extras), **kwargs (a dict of keyword extras), keyword-only parameters, positional-only parameters, and default values. Combined, they let you design signatures that are both flexible and precise.

def fn(a, b, *args, **kwargs): accepts any number of extra positionals as args and any extra keywords as kwargs. Inside the function they are just a tuple and a dict. You can unpack them back out when calling another function: other(*args, **kwargs). That is how decorators and wrappers pass arguments through without knowing what they are.

Keyword-only parameters (def fn(a, *, flag=True):) must be passed by name. That makes calls self-documenting and prevents positional mistakes. Positional-only parameters (def fn(a, /, b):) go the other way — they can't be passed by name — useful when the parameter name is an implementation detail you might rename. Together with defaults they give you fine-grained control over how callers invoke a function.

Unpacking at the call site is the mirror image: fn(*my_list, **my_dict) spreads a list into positional arguments and a dict into keyword arguments. That pattern makes it trivial to forward partially-built argument bundles or load call arguments from configuration.

*args, **kwargs in definitions and calls

Inside a function, args is a tuple and kwargs is a dict. Outside, * unpacks a sequence into positional args and ** unpacks a mapping into keyword args. Same symbol, two contexts.

Never mutate the incoming containers if callers might reuse them. Copy if you plan to change.

Keyword-only and positional-only

Anything after a bare * in the signature is keyword-only. Anything before a / is positional-only. Use keyword-only for booleans and rarely-changed options (clearer at call sites); positional-only for arguments like x, y that clearly belong in a particular slot.

Defaults only apply when the argument is omitted. Mutable defaults (def f(x=[])) are the most famous Python gotcha — use x=None and x = [] if x is None else x.

Argument-handling tools.

ToolPurpose
*args
syntax
Capture extra positional args.
**kwargs
syntax
Capture extra keyword args.
def f(*, flag=True)
syntax
Keyword-only parameters (PEP 3102).
def f(x, /, y)
syntax
Positional-only parameters (PEP 570).
f(*list, **dict)
syntax
Unpack at a call site.
inspect.signature
function
Inspect a function's parameters.
functools.partial
function
Pre-bind some arguments.
calls reference
docs
Full grammar of calls.

Creating Flexible Input Handling code example

The script shows *args/**kwargs, keyword-only flags, safe default handling, and forwarding through a wrapper.

# Lesson: Creating Flexible Input Handling
from functools import wraps


def summarize(*values, sep=", ", prefix="", uppercase=False):
    """Join any number of values into one string."""
    text = sep.join(str(v) for v in values)
    if uppercase:
        text = text.upper()
    return prefix + text


print(summarize("ana", "ben", "cai"))
print(summarize(1, 2, 3, sep=" + ", prefix="sum = "))
print(summarize("hi", "there", uppercase=True))


def style(text: str, /, *, bold: bool = False, color: str = "default") -> str:
    """Text is positional-only; options are keyword-only."""
    return f"[{color}{'/bold' if bold else ''}] {text}"


print(style("hello"))
print(style("hi", bold=True, color="red"))

try:
    style(text="nope")     # positional-only can't be passed by name
except TypeError as err:
    print("type error:", err)


# Safe mutable default
def collect(*items, into=None):
    if into is None:
        into = []
    into.extend(items)
    return into


a = collect(1, 2)
b = collect(9, 10)
print("a:", a, "| b:", b)   # both independent lists


# Forwarding: decorator wraps any function
def trace(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"-> {func.__name__}({args}, {kwargs})")
        return func(*args, **kwargs)
    return wrapper


@trace
def add(a, b, *, scale=1):
    return (a + b) * scale


print("add:", add(2, 3, scale=10))

# Unpacking at call site
args = (4, 5)
opts = {"scale": 2}
print("unpacked:", add(*args, **opts))

Reading top to bottom:

1) `*values, sep=` mixes arbitrary positionals with named options.
2) `/` makes `text` positional-only; `*,` makes `bold`/`color` keyword-only.
3) Mutable default pattern: `None` sentinel plus in-body assignment.
4) `*args`, `**kwargs` forwarding is the core of every decorator.

Accept either a list or multiple values.

def mean(*values):
    if len(values) == 1 and isinstance(values[0], (list, tuple)):
        values = values[0]
    if not values:
        raise ValueError("no values")
    return sum(values) / len(values)

print(mean(1, 2, 3))         # 2.0
print(mean([10, 20, 30]))    # 20.0

Invariants of a flexible signature.

def f(*a, **k):
    return a, k
assert f(1, 2, x=3) == ((1, 2), {"x": 3})
args, kw = (1, 2), {"x": 3}
assert f(*args, **kw) == ((1, 2), {"x": 3})

Running prints:

ana, ben, cai
sum = 1 + 2 + 3
HI, THERE
[default] hello
[red/bold] hi
type error: style() got some positional-only arguments passed as keyword arguments: 'text'
a: [1, 2] | b: [9, 10]
-> add((2, 3), {'scale': 10})
add: 50
-> add((4, 5), {'scale': 2})
unpacked: 18