What you'll learn
- Accept any number of positional arguments with
*args - Accept any number of keyword arguments with
**kwargs - Force callers to use keywords with
*, or positions with/ - Unpack a list or dictionary into a call with
*and**
Concept
Some functions cannot know in advance how many values they will get. print takes one, three, or twenty. max takes a whole list or several separate values. Python supports this with a star in the parameter list: def total(*numbers): collects every positional argument into a tuple called numbers. The name args is only a convention; the star is what matters.

Two stars do the same for keyword arguments: def show(**options): collects show(color="red", size=3) into a dictionary {"color": "red", "size": 3}. A function can have ordinary parameters, then *args, then **kwargs, in that order. This is the shape of every wrapper function you will read in libraries, which is why it is worth knowing early even though you will write it rarely.

A bare * in a parameter list, with no name, marks everything after it as keyword-only. def connect(host, *, timeout=30, retries=3) means callers must write timeout=10, never a stray 10 whose meaning you would have to look up. A / marks everything before it as positional-only, which library authors use to keep parameter names private. You will use * in your own code; / you will mostly read.
| Signature | Valid call | Invalid call |
|---|---|---|
def f(*args) |
f(1, 2, 3) |
f(a=1) |
def f(**kwargs) |
f(a=1, b=2) |
f(1) |
def f(host, *, timeout=30) |
f("db", timeout=5) |
f("db", 5) |
def f(x, /, y) |
f(1, 2) or f(1, y=2) |
f(x=1, y=2) |
The same stars work in the other direction, at the call site. total(*[3, 5, 8]) spreads a list into separate arguments, and line(**{"name": "tea", "price": 2}) spreads a dictionary into keyword arguments. This is how you forward a bundle of settings from one function to another, and how you turn data that arrived as a list into a call that wanted separate values.

Code
Collecting positional arguments. numbers is an ordinary tuple inside the function, so sum and len work on it, and calling with nothing gives an empty tuple.
# star_args.py
def total(*numbers):
print("got", numbers)
return sum(numbers)
print(total(3, 5, 8))
print(total())
print(total(1.5, 2.5))
got (3, 5, 8)
16
got ()
0
got (1.5, 2.5)
4.0Collecting keyword arguments, and the full order of parameter kinds in one signature.
# star_kwargs.py
def describe(title, *tags, **options):
print("title:", title)
print("tags:", tags)
print("options:", options)
describe("Python course", "beginner", "video", level=1, free=True)
title: Python course
tags: ('beginner', 'video')
options: {'level': 1, 'free': True}Keyword-only parameters. The first call is clear at a glance; the second is refused, which is exactly the protection you wanted.
# keyword_only.py
def connect(host, *, timeout=30, retries=3):
return f"{host} timeout={timeout} retries={retries}"
print(connect("db.local", timeout=5))
print(connect("db.local", 5))
db.local timeout=5 retries=3
Traceback (most recent call last):
File "keyword_only.py", line 5, in <module>
print(connect("db.local", 5))
~~~~~~~^^^^^^^^^^^^^^^
TypeError: connect() takes 1 positional argument but 2 were givenUnpacking at the call site. A list becomes positional arguments and a dictionary becomes keyword arguments, so settings can travel as data and land as a normal call.
# unpack_call.py
def line(name, price, currency="$"):
return f"{name:<8}{currency}{price:>6.2f}"
row = ["tea", 2.0]
settings = {"currency": "€"}
print(line(*row))
print(line(*row, **settings))
print(max(*[4, 9, 2]))
tea $ 2.00
tea € 2.00
9Try it yourself
Create logger.py with a function log(level, *messages, sep=" ", **context) that prints the level in upper case, the messages joined with sep, and then each context key and value on its own indented line. Call it as log("info", "server", "started", port=8000, debug=False). Then store a message list and a context dictionary in variables and call log by unpacking both.
Common mistakes
- Thinking the names matter.
*argsand**kwargsare conventions;*numbersand**optionswork identically, and descriptive names are better. - Putting
*argsafter**kwargs. The order is fixed: ordinary parameters,*args, keyword-only parameters,**kwargs. - Unpacking the wrong shape.
line(**row)with a list fails because two stars need a mapping; one star for sequences, two for dictionaries.
What's next
*namegathers extra positional arguments into a tuple;**namegathers extra keywords into a dictionary.- A bare
*makes the parameters after it keyword-only, which keeps calls readable;/makes earlier ones positional-only. - At a call,
*sequenceand**mappingspread a collection into arguments, which is how settings travel as data.
Functions can now take any shape of input. Next you will let a function call itself, and learn when that is the clearest solution and when a loop is better.