Imagine a function that should sum any number of arguments — 2, 5, 100. Without *args, you'd need overloads:
def total2(a, b): ...
def total3(a, b, c): ...
def total4(a, b, c, d): ...Awful. There must be a single-definition way.
*args collects extra positional arguments into a tuple inside the function:
def total(*nums):
return sum(nums)
total(1, 2) # 3
total(1, 2, 3, 4) # 10
total() # 0nums inside the function body is just a tuple — you can iterate, index, take its len. The * is only at the def site (and at certain call sites).
And keyword arguments — same idea?
**kwargs collects extra keyword arguments into a dict:
def show(**fields):
for k, v in fields.items():
print(f"{k}: {v}")
show(name="Ada", age=36)
# name: Ada
# age: 36And both at once?
Yes, in one signature: def f(*args, **kwargs). By convention these names are args and kwargs, but you can call them anything — the * and ** are what matter.
When do I use these in practice?
Most often when forwarding arguments to another function — your function takes whatever the caller throws at it and passes it on. We'll see that pattern in week 4. Today: just see the basic shape.
*args — extra positionals collected into a tupledef f(*args):
print(type(args), args)
f(1, 2, 3)
# <class 'tuple'> (1, 2, 3)Inside the function, args is a regular tuple. Iterate with for x in args, slice with args[1:], count with len(args).
*argsRequired parameters first, *args after them:
def log(level, *messages):
for m in messages:
print(f"[{level}] {m}")
log("INFO", "started", "ready", "done")
# [INFO] started
# [INFO] ready
# [INFO] doneThe first positional binds to level; everything else goes into messages.
**kwargs — extra keyword args collected into a dictdef show(**kwargs):
print(type(kwargs), kwargs)
show(name="Ada", age=36)
# <class 'dict'> {'name': 'Ada', 'age': 36}Inside the function, kwargs is a regular dict.
def f(*args, **kwargs):
print("args:", args)
print("kwargs:", kwargs)
f(1, 2, name="Ada")
# args: (1, 2)
# kwargs: {'name': 'Ada'}def f(required, optional=None, *args, kwonly_required, kwonly_default=0, **kwargs):
...The full order: required positionals → defaults → *args → keyword-only parameters → **kwargs. You won't write all six in one function — but knowing the order is enough to read other people's signatures.
*If you don't need *args but want some parameters to be keyword-only, use a bare * in the signature:
def f(a, b, *, strict=False): # strict must be passed by keyword
...
f(1, 2, strict=True) # OK
f(1, 2, True) # TypeError* and ** also work the other direction — spreading an existing list or dict into call arguments:
args = [1, 2, 3]
total(*args) # equivalent to total(1, 2, 3)
fields = {"name": "Ada", "age": 36}
show(**fields) # equivalent to show(name="Ada", age=36)Lesson 24 in week 4 builds on this for argument forwarding.
* in def vs calldef f(*args): ... # def: collect into tuple
f(*[1, 2, 3]) # call: spread list into positionalsSame symbol, opposite directions. Don't memorize names — read the position.
Imagine a function that should sum any number of arguments — 2, 5, 100. Without *args, you'd need overloads:
def total2(a, b): ...
def total3(a, b, c): ...
def total4(a, b, c, d): ...Awful. There must be a single-definition way.
*args collects extra positional arguments into a tuple inside the function:
def total(*nums):
return sum(nums)
total(1, 2) # 3
total(1, 2, 3, 4) # 10
total() # 0nums inside the function body is just a tuple — you can iterate, index, take its len. The * is only at the def site (and at certain call sites).
And keyword arguments — same idea?
**kwargs collects extra keyword arguments into a dict:
def show(**fields):
for k, v in fields.items():
print(f"{k}: {v}")
show(name="Ada", age=36)
# name: Ada
# age: 36And both at once?
Yes, in one signature: def f(*args, **kwargs). By convention these names are args and kwargs, but you can call them anything — the * and ** are what matter.
When do I use these in practice?
Most often when forwarding arguments to another function — your function takes whatever the caller throws at it and passes it on. We'll see that pattern in week 4. Today: just see the basic shape.
*args — extra positionals collected into a tupledef f(*args):
print(type(args), args)
f(1, 2, 3)
# <class 'tuple'> (1, 2, 3)Inside the function, args is a regular tuple. Iterate with for x in args, slice with args[1:], count with len(args).
*argsRequired parameters first, *args after them:
def log(level, *messages):
for m in messages:
print(f"[{level}] {m}")
log("INFO", "started", "ready", "done")
# [INFO] started
# [INFO] ready
# [INFO] doneThe first positional binds to level; everything else goes into messages.
**kwargs — extra keyword args collected into a dictdef show(**kwargs):
print(type(kwargs), kwargs)
show(name="Ada", age=36)
# <class 'dict'> {'name': 'Ada', 'age': 36}Inside the function, kwargs is a regular dict.
def f(*args, **kwargs):
print("args:", args)
print("kwargs:", kwargs)
f(1, 2, name="Ada")
# args: (1, 2)
# kwargs: {'name': 'Ada'}def f(required, optional=None, *args, kwonly_required, kwonly_default=0, **kwargs):
...The full order: required positionals → defaults → *args → keyword-only parameters → **kwargs. You won't write all six in one function — but knowing the order is enough to read other people's signatures.
*If you don't need *args but want some parameters to be keyword-only, use a bare * in the signature:
def f(a, b, *, strict=False): # strict must be passed by keyword
...
f(1, 2, strict=True) # OK
f(1, 2, True) # TypeError* and ** also work the other direction — spreading an existing list or dict into call arguments:
args = [1, 2, 3]
total(*args) # equivalent to total(1, 2, 3)
fields = {"name": "Ada", "age": 36}
show(**fields) # equivalent to show(name="Ada", age=36)Lesson 24 in week 4 builds on this for argument forwarding.
* in def vs calldef f(*args): ... # def: collect into tuple
f(*[1, 2, 3]) # call: spread list into positionalsSame symbol, opposite directions. Don't memorize names — read the position.
Create a free account to get started. Paid plans unlock all tracks.