Arguments in Depth

The Course
January 8, 2026
4 min read

Python’s argument rules are unusually flexible. Learning them is what lets you read other people’s code — and the standard library — without guessing.

Positional and keyword

Arguments are matched by position by default, or by name if you name them.

Two ways to pass the same values Python
def book_room(guest, nights, breakfast):
    return f"{guest}: {nights} nights, breakfast={breakfast}"

print(book_room("Ada", 3, True))                          # by position
print(book_room(guest="Ada", nights=3, breakfast=True))   # by name
print(book_room("Ada", breakfast=True, nights=3))         # mixed

# Named arguments may be reordered; positional ones may not.
print(book_room(3, "Ada", True))

That last line runs happily and produces nonsense — the classic argument-order bug. Naming arguments at the call site is free insurance, especially for booleans: book_room("Ada", 3, breakfast=True) says something that True alone does not.

*args — any number of positional arguments

Collecting extras Python
def total(*numbers):
    """Sum any number of arguments."""
    print("received:", numbers, type(numbers))
    return sum(numbers)

print(total(1, 2))
print(total(1, 2, 3, 4, 5))
print(total())

*numbers gathers every extra positional argument into a tuple. The name is up to you; args is simply the convention.

**kwargs — any number of named arguments

Collecting named extras Python
def describe(**details):
    print("received:", details)
    for key, value in details.items():
        print(f"  {key}: {value}")

describe(name="Ada", born=1815, field="mathematics")

**details gathers named arguments into a dictionary. Together, *args and **kwargs let a function accept anything — which is how decorators and wrapper functions work.

Everything at once Python
def report(title, *items, separator=", ", **meta):
    print(f"{title}: {separator.join(items)}")
    if meta:
        print("  meta:", meta)

report("Fruit", "apple", "pear", "fig")
report("Fruit", "apple", "pear", separator=" | ", source="market", fresh=True)

The order in a definition is fixed: normal parameters, then *args, then keyword-only parameters, then **kwargs.

Unpacking at the call site

The same two stars work in reverse — spreading a collection into arguments.

Spreading a list or dict into a call Python
def book_room(guest, nights, breakfast):
    return f"{guest}: {nights} nights, breakfast={breakfast}"

booking_values = ["Grace", 2, False]
print(book_room(*booking_values))          # list -> positional arguments

booking = {"guest": "Alan", "nights": 4, "breakfast": True}
print(book_room(**booking))                # dict -> keyword arguments

This is why you see f(*args, **kwargs) everywhere: it means “pass along whatever I was given, untouched”.

Mutable arguments

Arguments are passed by reference. Rebinding a name inside a function is local; mutating the object is visible to the caller.

What the caller sees Python
def rebind(items):
    items = ["new", "list"]      # only the local name changes
    return items

def mutate(items):
    items.append("added")        # the caller's list really changes
    return items

original = ["a", "b"]
rebind(original)
print("after rebind:", original)

mutate(original)
print("after mutate:", original)

Neither is wrong, but a function that quietly edits its arguments should say so in its name — sort_in_place(rows), not get_rows(rows). When in doubt, copy the input and return a new object.

Exercise

A flexible formatter

Write make_tag(name, *contents, **attributes) that builds an HTML-ish tag. make_tag("p", "Hello", "world", class_="intro") should print:

<p class="intro">Hello world</p>

Join the contents with spaces, and build each attribute as key="value" (strip any trailing underscore from the key, since class is a reserved word).

def make_tag(name, *contents, **attributes):
    # Your code here
    return ""

print(make_tag("p", "Hello", "world", class_="intro"))
print(make_tag("h1", "Title"))
Show one solution
def make_tag(name, *contents, **attributes):
    """Build a simple HTML tag from contents and attributes."""
    parts = ""
    for key, value in attributes.items():
        parts += f' {key.rstrip("_")}="{value}"'
    inner = " ".join(contents)
    return f"<{name}{parts}>{inner}</{name}>"

print(make_tag("p", "Hello", "world", class_="intro"))
print(make_tag("h1", "Title"))

This is close to how real templating helpers are written — *contents for however many children the caller has, **attributes for however many attributes.

What you learned

  • Arguments bind by position, or by name if you name them at the call site.
  • *args collects extra positional arguments into a tuple.
  • **kwargs collects extra named arguments into a dictionary.
  • * and ** at a call site spread a sequence or mapping into arguments.
  • Mutating an argument changes the caller’s object; rebinding does not.
Last updated on January 8, 2026

Was this article helpful?

Your response is saved on this device.