Function Arguments: Defaults, *args and **kwargs

Positional, keyword, default, variable length and keyword only arguments - five styles, one fixed order, and one famous trap involving mutable defaults.

Positional and keyword arguments

def describe(name, role, city):
    return f"{name} is a {role} in {city}"


print(describe("Meera", "engineer", "Pune"))                    # positional
print(describe(name="Meera", role="engineer", city="Pune"))     # keyword
print(describe("Meera", city="Pune", role="engineer"))          # mixed

Positional arguments are matched by order. Keyword arguments are matched by name, so their order is free. Once you use a keyword argument, everything after it must also be a keyword argument:

# print(describe(name="Meera", "engineer", "Pune"))
# SyntaxError: positional argument follows keyword argument

Default values

def greet(name, greeting="Hello", punctuation="!"):
    return f"{greeting}, {name}{punctuation}"


print(greet("Meera"))                          # Hello, Meera!
print(greet("Meera", "Welcome"))               # Welcome, Meera!
print(greet("Meera", punctuation="."))         # Hello, Meera.

Parameters with defaults must come after those without:

# def bad(greeting="Hello", name):
# SyntaxError: parameter without a default follows parameter with a default

Defaults are evaluated once

import time


def stamp(label, when=time.time()):     # evaluated when def RUNS
    return f"{label}: {when}"


print(stamp("first"))
time.sleep(1)
print(stamp("second"))       # the same timestamp - it was captured once

The mutable default argument trap

def add_item(item, basket=[]):        # WRONG
    basket.append(item)
    return basket


print(add_item("pen"))       # ['pen']
print(add_item("book"))      # ['pen', 'book']  <- the same list came back
print(add_item("bag"))       # ['pen', 'book', 'bag']

The empty list is created once, when the def statement executes, and every call that omits the argument shares it. The fix never varies:

def add_item(item, basket=None):      # correct
    if basket is None:
        basket = []
    basket.append(item)
    return basket


print(add_item("pen"))       # ['pen']
print(add_item("book"))      # ['book']
Rule: a default value may be a number, a string, a tuple, None or another immutable object. Never a list, dictionary, set or a call that produces a fresh object.

*args - any number of positional arguments

def total(*numbers):
    print(type(numbers))       # <class 'tuple'>
    return sum(numbers)


print(total())                 # 0
print(total(1, 2, 3))          # 6
print(total(*[4, 5, 6]))       # 15 - a list spread into arguments


def log(level, *messages):
    for message in messages:
        print(f"[{level}] {message}")


log("INFO", "started", "connected", "ready")

*args collects the extra positional arguments into a tuple. The name args is a convention; the star is the syntax.

**kwargs - any number of keyword arguments

def configure(**options):
    print(type(options))       # <class 'dict'>
    for key, value in options.items():
        print(f"{key} = {value}")


configure(theme="dark", size=14)

settings = {"theme": "light", "wrap": True}
configure(**settings)          # a dictionary spread into keyword arguments

The full parameter order

def f(pos_only, /, standard, *args, kw_only, **kwargs):
        |            |        |        |         |
        |            |        |        |         +-- extra keyword arguments
        |            |        |        +------------ must be given by name
        |            |        +--------------------- extra positional arguments
        |            +------------------------------ positional or keyword
        +------------------------------------------- positional only
def report(title, *sections, author="unknown", **metadata):
    print(f"{title} by {author}")
    for section in sections:
        print("  -", section)
    for key, value in metadata.items():
        print(f"  {key}: {value}")


report("Q3 Review", "Summary", "Findings", author="Meera", version=2, draft=True)

Keyword only parameters

def connect(host, port, *, timeout=30, retries=3):
    return f"{host}:{port} timeout={timeout} retries={retries}"


print(connect("localhost", 8080, timeout=5))
# print(connect("localhost", 8080, 5))     # TypeError: too many positional arguments

Everything after a bare * must be passed by name. Use this for options, so that a call site never reads as connect("localhost", 8080, 5, 2) with no clue what the numbers mean.

Positional only parameters

def distance(x, y, /):
    return abs(x - y)


print(distance(3, 10))          # 7
# print(distance(x=3, y=10))    # TypeError: positional only

Everything before a / must be passed by position. Since Python 3.8. It is used mainly by library authors who want to keep parameter names free to rename later.

Forwarding arguments

def log_call(func, *args, **kwargs):
    print(f"calling {func.__name__} with {args} {kwargs}")
    result = func(*args, **kwargs)
    print(f"  -> {result}")
    return result


def area(width, height=1):
    return width * height


log_call(area, 4, height=5)

Collecting with *args, **kwargs and passing them straight on is the standard wrapper pattern, and it is exactly how decorators work.

Common mistakes

  • Using a mutable default value.
  • Putting a positional argument after a keyword argument at the call site.
  • Defining a parameter with a default before one without.
  • Confusing *args at the definition (collect) with *list at the call (spread).
  • Passing a list to *args without the star, so the whole list becomes one argument.
  • Using **kwargs everywhere, hiding the real interface from readers and editors.

Best practices

  • Default mutable arguments to None and build the real value inside.
  • Use keyword only parameters for anything optional, especially booleans and numbers.
  • Name arguments at the call site when the value alone is not self explanatory.
  • Prefer explicit parameters to **kwargs; use **kwargs only for genuine pass through.
  • Keep the parameter count small. Four or more usually means a dataclass is waiting to be created.

Practice

  1. Write a function accepting any number of numbers and returning their mean, handling the empty case.
  2. Demonstrate the mutable default trap across three calls, then fix it.
  3. Write a function with two required parameters and three keyword only options.
  4. Write a wrapper that logs every call to any function and forwards all arguments.
  5. Explain the difference between f(*items) at a call and def f(*items) at a definition.

Conclusion

Five argument styles, one fixed order, and one rule that prevents the classic bug: never use a mutable default. Use keyword only parameters for options and your call sites will explain themselves.

Written by Lorens Mishra

Software Engineer Notes Management System Administrator

Continue reading

All Python notes →
Python

Scope and the LEGB Rule

Python resolves a name by looking in four places in a fixed order: local, enclosing, global, built-in. Assignment anywhere in a function makes the nam...

Read more
Python

Lambda Functions

A lambda is a small anonymous function written as a single expression. It exists for the places where naming a function would add nothing.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.