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

Default administrator account created by the installer.

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.