One quick reminder from lesson 1-2 before the new material. [n for n in [3, 8, 5, 10] if n > 4] evaluates to [8, 5, 10]. The trailing if is a filter, so every item greater than 4 survives, and that includes 5, which is easy to skip over on a fast read.
Comprehensions show up constantly in this unit, so it is worth having that shape at your fingertips before adding flexible function signatures on top of it.
Functions that take any number of arguments
This unit upgrades how you write functions themselves. Flexible signatures exist because a function often cannot know in advance how many values its caller has, and because generic wrapper code, like the decorators you will build in unit 5, must forward whatever arguments it receives without listing them. *args and **kwargs are how Python expresses both.
In Python for Beginners every function had a fixed parameter list: def greet(name, age) takes exactly two values. But print clearly does not work that way. print(1), print(1, 2), and print(1, 2, 3) are all valid calls to the same function, and the mechanism behind that is available to your own code.
Put a * before a parameter name and it collects all remaining positional arguments into a tuple:
def add_all(*nums): total = 0 for n in nums: total += n return total
Inside the function, nums is just a tuple you can loop over, index, or pass to len. By convention the parameter is named args, but any name works, and a descriptive name like nums is usually kinder to the next reader.
One function, any number of arguments
The same function handles two arguments, four arguments, or none at all, because every positional value the caller passes lands in one tuple.
def add_all(*nums): print("got the tuple:", nums) return sum(nums) print(add_all(1, 2)) print(add_all(10, 20, 30, 40)) print(add_all())
Output
got the tuple: (1, 2) 3 got the tuple: (10, 20, 30, 40) 100 got the tuple: () 0
The zero-argument call is the interesting one. nums becomes the empty tuple () rather than raising an error, and sum(()) is 0, so the function degrades gracefully instead of blowing up on an empty input.
**kwargs collects keyword arguments
Two stars do the same trick for keyword arguments (the name=value style you used when calling functions). They land in a dict:
def make_profile(**info): return info make_profile(name="Ada", role="engineer") # {'name': 'Ada', 'role': 'engineer'}
The stars also work in reverse when calling. * unpacks a list into separate positional arguments, ** unpacks a dict into keyword arguments:
nums = [3, 5] print(*nums) # same as print(3, 5)
Full signature order when you combine everything: normal params, *args, then **kwargs.
describe
describe(name, **details) takes one required positional argument and sweeps up every keyword argument into a dict. It prints the name, then one key: value line per keyword argument, looping with details.items() just as you would over any other dict.
def describe(name, **details): print(name) for key, value in details.items(): print(f"{key}: {value}") describe("laptop", brand="Lenovo", ram=16)
Output
laptop
brand: Lenovo
ram: 16Inside the body, details is nothing exotic: it is a plain dict holding {'brand': 'Lenovo', 'ram': 16}. That means details.items() hands you both halves of each pair, and an f-string like f"{key}: {value}" formats each line. The caller gets to invent field names on the spot, and the function never needs updating to accept a new one.
area
Stars work in the other direction too. At a call site, * spreads a sequence into positional arguments and ** spreads a dict into keyword arguments. The two calls below reach the same result from a list and from a dict, with no numbers typed by hand.
def area(width, height): return width * height dims = [4, 5] settings = {"width": 2, "height": 10} print(area(*dims)) print(area(**settings))
Output
20 20
area(*dims) spreads the two-item list into the two positional parameters, so it behaves exactly like area(4, 5). area(**settings) turns each dict key into a keyword argument, which is why the keys have to match the parameter names spelled in the def line. A key named w instead of width would raise a TypeError.
The two types to keep straight
Inside def f(*args, **kwargs), args is always a tuple and kwargs is always a dict. One star gathers positional arguments, two stars gather keyword arguments.
| Form | In the signature | In a call |
|---|---|---|
* | gathers positionals into a tuple | spreads a sequence into positionals |
** | gathers keywords into a dict | spreads a dict into keywords |
Remembering the types tells you immediately what you are allowed to do with each one. You can loop over args and index it, and you can call .items() on kwargs or look a name up in it.