*args and **kwargs
print() takes as many values as you like. max() does too. You can write
functions that do the same, and you will meet the syntax constantly in other
people's code, so it is worth being able to read even before you need to write
it.
*args
A parameter prefixed with * collects every extra positional argument into a
tuple:
def total(*numbers):
return sum(numbers)
print(total(1, 2))
print(total(1, 2, 3, 4, 5))
print(total())
3
15
0
Inside the function, numbers is a tuple — (1, 2), then (1, 2, 3, 4, 5),
then (). It is always a tuple, even with one argument or none, so you can
safely loop over it or call len() without checking.
The * is the syntax; args is just a conventional name. *numbers is
better here because it says what the values are. Use *args only when the
values genuinely have no more specific name.
Ordinary parameters can come first:
def announce(prefix, *names):
for name in names:
print(f"{prefix}: {name}")
announce("Attending", "Priya", "Arjun", "Sneha")
Attending: Priya
Attending: Arjun
Attending: Sneha
prefix takes the first argument; *names collects the rest. Only one *
parameter is allowed, and everything after it must be keyword-only — which is
the same mechanism as the bare * from the arguments lesson.
**kwargs
Two asterisks collect every extra keyword argument into a dictionary:
def describe(**details):
for key, value in details.items():
print(f"{key}: {value}")
describe(name="Priya", age=28, city="Pune")
name: Priya
age: 28
city: Pune
details is a dictionary. Again, kwargs is convention, not syntax.
Both together
The full form, which you will recognise in library code:
def log(level, *messages, **context):
print(f"[{level}]", *messages)
for key, value in context.items():
print(f" {key}={value}")
log("ERROR", "Database unreachable", host="db01", retries=3)
[ERROR] Database unreachable
host=db01
retries=3
The order in a definition is always: normal parameters, *args, keyword-only
parameters, **kwargs. You cannot rearrange it.
Note print(f"[{level}]", *messages) — that is * at a call site, spreading
the tuple back into separate arguments. Which brings us to the symmetry.
The two meanings of * and **
This is the part that makes it click.
In a definition, they collect:
def f(*args, **kwargs):
...
At a call, they spread:
values = [1, 2, 3]
f(*values) # f(1, 2, 3)
options = {"a": 1, "b": 2}
f(**options) # f(a=1, b=2)
Same symbols, opposite directions. Collecting on the way in, spreading on the way out.
That symmetry gives you the most common real use — passing arguments straight through:
def log_call(func, *args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
print(log_call(max, 3, 7, 2))
Calling max
7
log_call collects whatever it is given and hands it on untouched, without
knowing or caring what func expects. This is how wrappers and decorators work,
and why *args, **kwargs appears in so much framework code.
Note also that functions are values here: max was passed without brackets,
stored in func, and called later. A function with brackets runs; without, it
is a thing you can pass around. That is the same distinction as module 1,
finally paying off.
lambda
Since functions are values, you sometimes want one where writing a whole def
would be more ceremony than it is worth. lambda makes a small unnamed
function:
double = lambda n: n * 2
print(double(5)) # 10
The form is lambda parameters: expression. There is no return — the
expression is the return value — and it must be a single expression, so no
if statements, no loops, no multiple lines.
That example is also bad practice. If you are assigning a lambda to a name, use
def, which gives you a docstring, a proper name in tracebacks, and room to
grow:
def double(n: int) -> int:
return n * 2
Where lambda genuinely earns its place is as a throwaway argument to another
function — which is the key promised back in the list-methods lesson:
people = [("Priya", 28), ("Arjun", 34), ("Sneha", 25)]
print(sorted(people, key=lambda person: person[1]))
[('Sneha', 25), ('Priya', 28), ('Arjun', 34)]
key calls that lambda on each item and sorts by what comes back — here, the
age. Writing a named function for something used once, on one line, would be
worse.
More of the same:
words = ["banana", "fig", "cherry"]
print(sorted(words, key=lambda w: len(w)))
students = [{"name": "Priya", "mark": 78}, {"name": "Arjun", "mark": 92}]
print(sorted(students, key=lambda s: s["mark"], reverse=True))
print(max(students, key=lambda s: s["mark"])["name"])
That last line finds the top student in one expression.
Use lambda only as an argument, and only when it fits on the line. The
moment it needs a condition or a second thought, write a def and pass its
name — key=get_mark reads better than any lambda ever will.
When to use them
Less often than beginners expect.
Good reasons:
- The number of values genuinely varies —
total(*numbers). - You are wrapping another function and must pass everything through.
- You are accepting optional extras to forward elsewhere.
Bad reasons:
def create_user(**kwargs):
name = kwargs["name"]
email = kwargs["email"]
That takes a function with two clear required parameters and makes it accept anything, fail at runtime instead of at the call, and tell the reader nothing. Worse, an editor can no longer autocomplete or check the call.
Named parameters are documentation. Write them out whenever you know what they are:
def create_user(name, email, is_admin=False):
...
Use *args and **kwargs when you genuinely do not know, not to avoid deciding.
Reading them in the wild
Given the above, this is now readable:
def retry(func, *args, attempts=3, **kwargs):
for attempt in range(attempts):
result = func(*args, **kwargs)
if result is not None:
return result
return None
Takes a function, any positional arguments for it, a keyword-only attempts
with a default, and any keyword arguments for it. Calls it up to attempts
times, passing everything through, and returns the first non-None result.
Note attempts sits after *args, making it keyword-only — so it cannot be
swallowed by *args as one more argument for func. That placement is
deliberate, and recognising why is a sign this has landed.
Practice
- Write
total(*numbers)and call it with zero, one and five arguments. Printtype(numbers)inside to confirm it is always a tuple. - Write
average(*numbers)that returns 0 for no arguments rather than crashing on division by zero. - Write
announce(prefix, *names)and call it with several names. - Write
describe(**details)printing each key and value. Call it with three keyword arguments, then with none. - Build a list and a dictionary, and use
*and**to call an ordinary three-parameter function with them. - Write
log_call(func, *args, **kwargs)as above, and use it to callmax,minandsorted. Notice it needs no changes between them. - Rewrite
def create_user(**kwargs)with named parameters. Say what improved. - Explain in one sentence why
attemptsin theretryexample must come after*args. - Sort a list of
(name, age)tuples by age usinglambda. Then sort a list of dictionaries by a key, descending. - Assign a lambda to a name, then rewrite it as a
def. Say which you would rather see in a code review.
Next: docstrings and type hints — making a function explain itself.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship