Python function arguments, explained
Python's argument system is unusually flexible — positional, keyword, variadic, keyword-only, positional-only — and that flexibility is a frequent interview topic. Get the order and the mutable-default trap right and you'll handle almost any signature question.
Positional vs keyword arguments
When calling a function, you can pass arguments by position or by name:
def greet(name, greeting):
return f"{greeting}, {name}"
greet("Ada", "Hi") # positional — order matters
greet(greeting="Hi", name="Ada") # keyword — order doesn't, clarity wins
Keyword arguments are self-documenting and immune to argument-order mistakes, so prefer them for anything non-obvious (especially booleans and numbers).
Default argument values
A parameter with a default becomes optional. Defaults must come after all non-default parameters:
def connect(host, port=5432, timeout=30):
...
connect("db.example.com") # uses both defaults
connect("db.example.com", timeout=5) # override just one by name
The mutable default argument trap
This is the single most famous Python gotcha. A default value is evaluated once, when
the function is defined — not on each call. A mutable default (like []) is therefore
shared across all calls:
def add_item(item, items=[]): # BUG
items.append(item)
return items
add_item("a") # ['a']
add_item("b") # ['a', 'b'] — the same list persists!
The fix is to default to None and create a fresh object inside:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
*args — variadic positional arguments
*args collects extra positional arguments into a tuple, letting a function take any
number of them:
def total(*nums):
return sum(nums)
total(1, 2, 3) # 6 — nums is (1, 2, 3)
total() # 0 — nums is ()
**kwargs — variadic keyword arguments
**kwargs collects extra keyword arguments into a dict:
def configure(**options):
return options
configure(debug=True, level=3) # {'debug': True, 'level': 3}
Together, *args, **kwargs capture any call — which is exactly why decorator wrappers use
them to forward arguments transparently.
Keyword-only arguments
A bare * in the signature forces every parameter after it to be passed by keyword.
This prevents confusing positional calls:
def create_user(name, *, admin=False, active=True):
...
create_user("Ada", admin=True) # OK
create_user("Ada", True) # TypeError — admin is keyword-only
Positional-only arguments
A / in the signature (3.8+) forces parameters before it to be passed by position —
useful for parameters whose names are implementation details:
def divide(a, b, /):
return a / b
divide(10, 2) # OK
divide(a=10, b=2) # TypeError — a and b are positional-only
The full parameter order
Putting it all together, a complete signature reads in this fixed order:
def f(pos_only, /, normal, *args, kw_only, **kwargs):
...
# positional-only | normal | *args | keyword-only | **kwargs
You rarely use all five at once, but knowing the order resolves any "where does this go?" question.
Recap
Arguments can be positional or keyword; defaults come last and are evaluated once
at definition — so never use a mutable default, use None and create the object inside.
*args gathers extra positionals into a tuple, **kwargs gathers extra keywords into a
dict, and together they forward any call. A bare * makes the following parameters
keyword-only, a / makes the preceding ones positional-only, and the full order is
positional-only → normal → *args → keyword-only → **kwargs.