Flexible arguments: *args, **kwargs and the default trap
Part of the free LogicWiz Python course, Module 11: Functions in depth. The examples and practice run in the app.
Shankar bhaiya wants bill() to work for one item or ten. And Aarav's add_item() keeps showing yesterday's chai on today's bill. Both problems live in the function's brackets.
You'll learn to
- Accept any number of values with `*args` and any named values with `**kwargs`
- Force named arguments with a bare `*` (keyword-only)
- Unpack a list or dict into a call with `*` and `**`
- Spot the mutable default argument trap and fix it with `None`
Learn
*args: any number of values
Put a * before a parameter name and Python collects all the extra positional values into a tuple.
The name args is only a habit. *prices works the same and reads better.
With no extra values, you get an empty tuple (), not an error.
One bill function for any number of items
def total(*prices):
print("prices =", prices)
return sum(prices)
print(total(20, 15, 10))
print(total(30))
print(total())
**kwargs and the order of parameters
Two stars collect the extra named values into a dict: notes={'spicy': 'extra'}.
The order inside the brackets is fixed:
def f(normal, *args, keyword_only=0, **kwargs)
Anything written after *args (or after a bare *) can only be passed by name. That is a keyword-only argument. It stops mistakes like pay(120, 'cash') where nobody can tell what 'cash' means.
All four kinds together
def order(table, *items, paid=False, **notes):
print("Table", table, "ordered", items)
print("Paid?", paid)
for key, value in notes.items():
print(" ", key, "->", value)
order(4, "samosa", "chai", spicy="extra", sugar="less")
order(7, "vada pav", paid=True)
paid comes after *items, so it is keyword-only: order(7, "vada pav", True) would make True another item.
Unpacking: * and ** in a call
def bill(price, qty, gst=5):
return round(price * qty * (1 + gst / 100), 2)
item = [15, 4] # price, qty
print(bill(*item)) # same as bill(15, 4)
extras = {"gst": 12}
print(bill(*item, **extras)) # same as bill(15, 4, gst=12)
first, *rest = [10, 20, 30, 40]
print(first, rest)
In a definition, * collects. In a call, * spreads. Same symbol, opposite jobs.
The trap: a list as a default
def add_item(item, cart=[]):
cart.append(item)
return cart
print(add_item("chai"))
print(add_item("samosa")) # a new customer!
Step through it: both calls point at the same list object.
The fix: default to None
def add_item(item, cart=None):
if cart is None:
cart = [] # a fresh list on every call
cart.append(item)
return cart
print(add_item("chai"))
print(add_item("samosa"))
print(add_item("vada", ["chai"]))
Default values are made once, when def runs, not on every call. A list, dict or set default is therefore shared by every call that doesn't pass its own. Use None as the default and create the list inside. Numbers, strings and tuples are safe defaults because they can't change.
Interviewers at TCS, Infosys and product companies love two questions here: "What are *args and **kwargs?" and "What does this print?" with a list default. Both answers are in this lesson.
In one line
*args collects extra values into a tuple, **kwargs into a dict; never use a mutable default, use None and create it inside.
Interview corner
What are *args and **kwargs?
*args collects extra positional arguments into a tuple; **kwargs collects extra keyword arguments into a dict. The names are a convention, the stars do the work. In a call, * and ** do the opposite: they unpack a list or dict into arguments, e.g. f(*[1, 2], **{"gst": 5}).
Why is a mutable default argument dangerous?
Default values are evaluated once, when the def statement runs, and stored on the function. A list or dict default is therefore shared by every call that does not pass its own, so data leaks between calls. Use None and create the object inside: if cart is None: cart = [].
What do a bare * and a / mean in a parameter list?
Parameters after * are keyword-only: def pay(amount, *, method="UPI") must be called as pay(100, method="cash"). Parameters before / are positional-only: def f(a, /) cannot be called as f(a=1). Both make an API harder to misuse.