Function Argument Binding#

Parameter Collection#

In a function definition, * and ** collect additional arguments:

def process(*args, **kwargs):
    ...

args is a tuple containing additional positional arguments.

kwargs is a dictionary containing additional keyword arguments.

The names args and kwargs are conventions only. The * and ** syntax determines the behaviour.

Call-Site Unpacking#

At a call site, the same symbols perform unpacking:

process(*positional, **keywords)

*positional expands an iterable into positional arguments.

**keywords expands a mapping into keyword arguments. Its keys must be strings because they become keyword argument names.

For example:

def submit_order(symbol, quantity):
    ...


values = ("AAPL", 100)
submit_order(*values)

is equivalent to:

submit_order("AAPL", 100)

Keyword unpacking:

parameters = {
    "symbol": "AAPL",
    "quantity": 100,
}

submit_order(**parameters)

is equivalent to:

submit_order(
    symbol="AAPL",
    quantity=100,
)

Unpacking a Mapping#

Using * on a dictionary iterates over its keys:

parameters = {
    "symbol": "AAPL",
    "quantity": 100,
}

submit_order(*parameters)

This passes:

submit_order("symbol", "quantity")

It does not pass the dictionary values.

Use **parameters when the mapping represents keyword arguments.

Argument Forwarding#

A common forwarding pattern is:

def wrapper(*args, **kwargs):
    return target(*args, **kwargs)

The definition collects arguments into args and kwargs. The call then expands them again when invoking target.

Collection and unpacking therefore occur at different stages:

caller
  |
  |  target(*args, **kwargs)
  v
argument unpacking
  |
  v
parameter binding
  |
  v
def target(*args, **kwargs)
         |
         +-- collect excess positional arguments into a tuple
         +-- collect excess keyword arguments into a dictionary

Binding Errors#

Argument binding raises TypeError when the supplied arguments cannot be matched to the function parameters.

Common cases include:

  • too many positional arguments;

  • a required argument is missing;

  • an unexpected keyword argument is supplied;

  • the same parameter receives multiple values.

For example:

def f(quantity):
    ...


f(100, quantity=200)

raises TypeError because quantity receives both a positional and a keyword value.

Interview Notes#

Distinguish definition-side collection from call-side unpacking:

def f(*args, **kwargs):
    ...

f(*args, **kwargs)

In the definition:

  • *args collects positional arguments into a tuple;

  • **kwargs collects keyword arguments into a dictionary.

In the call:

  • *args expands an iterable into positional arguments;

  • **kwargs expands a mapping into keyword arguments.

Given a dictionary d, f(*d) passes its keys positionally, while f(**d) treats its keys as keyword argument names.