Python’s argument rules are unusually flexible. Learning them is what lets you read other people’s code — and the standard library — without guessing.
The parts of this you need for the rest of the beginner course are already in Defining Functions: naming an argument at the call site, and why a default must never be a list. What’s new here — collecting any number of arguments, and spreading a collection into a call — is how decorators and wrapper functions work, and the intermediate course leans on it.
Positional and keyword
Arguments are matched by position by default, or by name if you name them.
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
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
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.
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.
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 argumentsThis 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.
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.
Before you write your own
Predict. How many positional arguments arrive each time, and which names?
def show(*args, **kwargs):
print(len(args), sorted(kwargs))
show(1, 2, 3)
show("a", size="L", colour="red")
show()Check your prediction
3 []
1 ['colour', 'size']
0 []Positional arguments land in the tuple args, named ones in the dictionary
kwargs — and sorted on a dictionary sorts its keys. Calling with nothing at
all is fine: both are simply empty.
Fix. This should say Move 50 from savings to current. Every argument is
there — in the wrong places. Fix it by naming them at the call, without
touching the function.
def transfer(amount, from_account, to_account):
return f"Move {amount} from {from_account} to {to_account}"
print(transfer("savings", "current", 50))Check your fix
def transfer(amount, from_account, to_account):
return f"Move {amount} from {from_account} to {to_account}"
print(transfer(50, from_account="savings", to_account="current"))Move 50 from savings to currentPython matched the values to the parameters by position and ran without complaint — which is what makes this kind of bug dangerous with money. Named arguments make the call say what it means.
A flexible formatter
Write make_tag(name, *contents, **attributes) that builds an HTML-ish tag.
The two calls in the playground should print:
<p class="intro">Hello world</p>
<h1>Title</h1>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"))Hint 1: the idea
Build two pieces of text, then put them together. contents is a tuple of
strings, so joining it with spaces gives the inside of the tag. attributes is
a dictionary: each key and value becomes one key="value" piece, and the
pieces are added together.
Hint 2: a first step
Make the simpler call work first — the one with no attributes. With
inner = " ".join(contents) and return f"<{name}>{inner}</{name}>", the second
line already prints <h1>Title</h1>. Then loop over attributes.items() to add
the rest, using key.rstrip("_") for each name.
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.
*argscollects extra positional arguments into a tuple.**kwargscollects 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.