*args, **kwargs &
Argument Rules in Python
Write functions that accept any number of arguments — positional, keyword, or both. Learn the rules Python enforces, why they exist, and how to read a function signature at a glance. Every example is runnable in your browser.
def greet(*args, **kwargs): for name in args: print(f"Hello, {name}!") for key, value in kwargs.items(): print(f" {key}: {value}") greet("Ana", "Carlos", role="admin", active=True)
Hello, Ana!
Hello, Carlos!
role: admin
active: True
📖 Introduction — Why Function Signatures Matter
Understand the rules before you break them
Every Python function you write has an argument list — the variables in parentheses after the function name. How you define that list determines what the function accepts, how callers must call it, and what happens when someone passes the wrong thing. Most bugs in beginner Python code trace back to a misunderstanding of one of these rules.
Python's argument system is unusually flexible. A function can take:
- ✓Required positional arguments — must be supplied in order
- ✓Optional arguments with default values — can be omitted
- ✓Positional-only arguments — cannot be passed by name
- ✓Keyword-only arguments — must be passed by name
- ✓Variable-length positional arguments —
*args, any number of positional values - ✓Variable-length keyword arguments —
**kwargs, any number of named values
The core mental model
A function signature is a contract. The definition says what the function accepts; the call site must honour it. Python checks this contract at call time — and the error messages (especially in Python 3.10+) are designed to tell you exactly what went wrong.
🔑 The four things you need to know first
Positional arguments are matched to parameters by position — first argument to first parameter, second to second, and so on.
Keyword arguments are matched by name — greet(name="Ana") passes "Ana" to the parameter name regardless of position.
Default values make a parameter optional. If the caller omits it, the default is used instead.
*args and **kwargs collect any extra values into a tuple and a dictionary respectively — useful when you don't know in advance how many arguments will be passed.
🧠 How Python matches arguments to parameters — the full picture
When you call a function, Python performs this sequence:
Assign all positional arguments to the leading positional parameters, in order.
Assign all keyword arguments to the named parameters — raising TypeError if a name doesn't match any parameter.
Collect any remaining positional values into *args (as a tuple), if the function defines it.
Collect any remaining keyword values into **kwargs (as a dict), if the function defines it.
Apply default values for any parameters not yet filled.
If any parameter still has no value, raise TypeError — the function is missing a required argument.
1️⃣ Positional Arguments — The Default
The simplest and most common kind of argument
Positional arguments are matched to parameters by order. The first value in the call goes to the first parameter in the definition, the second to the second, and so on. If you pass too few, Python raises an error; if you pass too many (and there's no *args), Python also raises an error.
📝 Example — a function with two required positional parameters
def describe_pet(animal_type, pet_name): print(f"I have a {animal_type} named {pet_name}.") describe_pet("dog", "Rex") describe_pet("cat", "Whiskers")
def describe_pet(animal_type, pet_name): defines a function with two required positional parameters. Both must be supplied.Line 2 — the f-string uses both parameters inside the function body.
Line 4 —
describe_pet("dog", "Rex") passes "dog" to animal_type and "Rex" to pet_name. The order matters: if you swapped them, you'd get "I have a Rex named dog."
❌ Wrong — too few arguments
describe_pet("dog") → TypeError: describe_pet() missing 1 required positional argument: 'pet_name'
✅ Correct — exactly two
describe_pet("dog", "Rex") → I have a dog named Rex.
Python 3.10+ error messages are noticeably better than earlier versions — they tell you which argument is missing, and for some errors suggest a fix. This alone is worth using a recent Python version.
🔄 Calling with keyword arguments instead
You can also call the same function using keyword arguments — passing values by parameter name. This makes the call more readable and the order no longer matters.
def describe_pet(animal_type, pet_name): print(f"I have a {animal_type} named {pet_name}.") describe_pet(animal_type="dog", pet_name="Rex") describe_pet(pet_name="Whiskers", animal_type="cat")
Line 5 — even reversed, the output is still correct: "I have a cat named Whiskers."
describe_pet(animal_type="dog", pet_name="Rex") is clearer than describe_pet("dog", "Rex") — someone reading the call doesn't have to check the definition to know which value is which.2️⃣ Keyword Arguments — Passing by Name
Clarity over brevity — call sites that explain themselves
A keyword argument is passed as name=value in the call. Python matches it to the parameter with the same name. This is especially useful when a function has many parameters, or when some parameters are optional and you only want to supply the ones that matter.
📝 Example — a function with many parameters
def create_profile(username, email, age, country="Unknown", newsletter=False): print(f"User: {username}") print(f"Email: {email}") print(f"Age: {age}") print(f"Country: {country}") print(f"Newsletter: {newsletter}") create_profile( username="ana_dev", email="ana@example.com", age=28, newsletter=True )
username, email, age) and two optional ones with defaults (country, newsletter).Lines 7–12 — the call passes
username, email, age, and newsletter by name. country is omitted, so its default value "Unknown" is used.Why this matters — without keyword arguments, the caller would have to remember the exact position of every parameter. With them, the call reads like a configuration block.
❌ Without keywords — hard to read
create_profile("ana_dev", "ana@example.com", 28, "Philippines", True)
✅ With keywords — self-documenting
create_profile(username="ana_dev", email="ana@example.com", age=28, country="Philippines", newsletter=True)
⚠️ Rule — positional arguments must come before keyword arguments
If you mix positional and keyword arguments in a single call, all positional arguments must come first. Python cannot tell where the positional values end and the keyword values begin otherwise.
def order_coffee(size, milk="whole", sugar="none"): print(f"Size: {size}, Milk: {milk}, Sugar: {sugar}") order_coffee("large", sugar="two")
"large" is positional, filling size. sugar="two" is keyword, filling sugar. milk is omitted and uses its default "whole".The rule — you cannot write
order_coffee(size="large", "two"). A positional argument after a keyword argument is a SyntaxError.
SyntaxError before the code even runs. That's intentional: the syntax itself teaches you the rule.3️⃣ Default Values — Optional Parameters
Make parameters optional — with one important caveat
A parameter with a default value is optional: if the caller doesn't supply it, Python uses the default. This is the cleanest way to extend a function's signature without breaking existing callers.
📝 Example — a simple optional parameter
def introduce(name, greeting="Hello"): print(f"{greeting}, {name}!") introduce("Ana") introduce("Carlos", greeting="Hi") introduce("Sam", "Hey there")
greeting has a default value of "Hello".Line 4 —
introduce("Ana") omits greeting, so the default is used: Hello, Ana!.Line 5 —
greeting="Hi" overrides the default: Hi, Carlos!.Line 6 —
"Hey there" is passed positionally — a default value doesn't force callers to use keyword syntax.
⚠️ The mutable default argument trap
Default values are evaluated once — when the function is defined, not each time it's called. This is harmless for immutable values like strings, numbers, and None. It is a serious bug for mutable values like lists, dictionaries, and sets.
def add_item_buggy(item, items=[]): items.append(item) return items print(add_item_buggy("apple")) print(add_item_buggy("banana")) def add_item_safe(item, items=None): if items is None: items = [] items.append(item) return items print(add_item_safe("apple")) print(add_item_safe("banana"))
[] is created once, when the function is defined. Both calls share the same list. The second call prints ['apple', 'banana'], not ['banana'].Lines 8–11 — the safe pattern: use
None as the default, and create a fresh list inside the function body. Each call gets its own list.Why
None works — None is immutable, so reusing the same None object across calls is harmless. The key is creating the mutable object inside the body.
if x is None: x = [] appears throughout Python's standard library for exactly this reason. It's worth learning as a reflex.4️⃣ *args — Variable-Length Positional Arguments
Accept any number of positional values, collected into a tuple
Sometimes you don't know in advance how many arguments a function will receive. The *args syntax collects any number of extra positional values into a tuple. The name args is a convention, not a keyword — you could call it *numbers or *items — but args is what everyone expects to see.
📝 Example — a function that sums any number of values
def sum_all(*args): print(f"Received {len(args)} numbers: {args}") return sum(args) print(sum_all(1, 2, 3)) print(sum_all(10, 20, 30, 40, 50)) print(sum_all())
*args collects all positional values passed to the function into a single tuple.Line 2 —
args is a tuple. len(args) gives the count; printing it shows the full tuple.Line 3 —
sum(args) works because args is just a tuple of numbers.Line 7 — calling with zero arguments is valid:
args is an empty tuple, and sum(()) returns 0.
🧩 How *args interacts with regular parameters
Any leading parameters are filled first. *args collects only the remaining positional values.
def describe_first_and_rest(first, *rest): print(f"First: {first}") print(f"Rest: {rest}") print(f"Number of rest items: {len(rest)}") describe_first_and_rest("apple", "banana", "cherry") describe_first_and_rest("only_one")
first is a regular parameter; *rest collects any additional positional values.Line 5 —
"apple" fills first. The remaining values "banana" and "cherry" are collected into rest as the tuple ('banana', 'cherry').Line 6 — when only one argument is passed,
rest is an empty tuple. len(rest) is 0.
⚠️ Common error — positional arguments after *args
Because *args greedily collects all remaining positional values, anything declared after it must be passed as a keyword argument. This is the foundation of keyword-only parameters.
def bad_ordering(*args, last): print(args, last) bad_ordering(1, 2, 3, last="end")
last comes after *args. It is keyword-only: it must be passed by name.Line 4 —
last="end" is required. If you tried bad_ordering(1, 2, 3, "end"), Python would raise TypeError: bad_ordering() missing 1 required keyword-only argument: 'last' — because the string "end" would be collected into args, not assigned to last.
5️⃣ **kwargs — Variable-Length Keyword Arguments
Accept any number of named values, collected into a dictionary
Just as *args collects extra positional values into a tuple, **kwargs collects extra keyword values into a dictionary. This is useful for functions that accept arbitrary configuration options, forward arguments to other functions, or build flexible APIs. The name kwargs is a convention — short for "keyword arguments" — but you'll occasionally see **options or **config.
📝 Example — building a configuration dictionary
def build_config(**kwargs): print(f"Received {len(kwargs)} options:") for key, value in kwargs.items(): print(f" {key} = {value}") build_config(debug=True, timeout=30, retries=3) build_config()
**kwargs collects all keyword arguments into a dictionary. Inside the function, kwargs behaves like any other dict.Line 2 —
len(kwargs) gives the number of keyword arguments supplied.Line 3 —
kwargs.items() iterates over key–value pairs, exactly as with any dictionary.Line 5 —
build_config(debug=True, timeout=30, retries=3) produces {'debug': True, 'timeout': 30, 'retries': 3}.Line 6 — calling with no keyword arguments is valid:
kwargs is an empty dict.
🧩 Mixing **kwargs with regular keyword parameters
If a function has regular keyword parameters before **kwargs, those parameters capture the named values that match them, and **kwargs collects the rest.
def create_user(name, role="user", **extra): print(f"Name: {name}") print(f"Role: {role}") print(f"Extra: {extra}") create_user("ana", role="admin", age=28, country="PH")
name is required positional; role is keyword with a default; **extra collects any remaining keyword arguments.Line 5 —
"ana" fills name. role="admin" fills role. age=28 and country="PH" don't match any declared parameter, so they go into extra.Output —
Extra: {'age': 28, 'country': 'PH'}.
🔁 Forwarding arguments — *args and **kwargs together
A common pattern is a wrapper function that accepts any arguments and forwards them to another function. This is how decorators work, and how you write functions that are transparent proxies.
def log_call(func, *args, **kwargs): print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}") result = func(*args, **kwargs) print(f"Result: {result}") return result def add(a, b, scale=1): return (a + b) * scale log_call(add, 3, 5, scale=2)
log_call accepts a function plus any positional and keyword arguments.Line 3 —
func(*args, **kwargs) unpacks the collected arguments and passes them to func. This is the critical line: it turns the tuple args back into positional arguments and the dict kwargs back into keyword arguments.Line 9 —
log_call(add, 3, 5, scale=2) inside log_call: func=add, args=(3, 5), kwargs={'scale': 2}. The forwarded call is add(3, 5, scale=2), which returns 16.
@wraps(func) and def wrapper(*args, **kwargs), this is exactly what's happening — the wrapper accepts anything and passes it through unchanged.6️⃣ Putting It All Together — A Realistic Function Signature
Every argument kind in one function, with a clear explanation of what each part does
Python lets you combine all the argument kinds in a single function. The order is fixed by the language — you can't rearrange them arbitrarily. This example shows a complete, realistic signature and explains each part.
📝 Example — a function with every argument kind
def create_report( title, /, author="Anonymous", *sections, format="pdf", **metadata ): print(f"Title: {title}") print(f"Author: {author}") print(f"Sections: {sections}") print(f"Format: {format}") print(f"Metadata: {metadata}") create_report( "Q2 Summary", "Ana", "Intro", "Methods", "Results", format="html", confidential=True )
title before / — positional-only. Callers must pass it as a positional argument; title="Q2 Summary" would be a TypeError.author="Anonymous" — can be passed positionally or by name; has a default so it's optional.*sections — collects any remaining positional values into a tuple. In the call, "Intro", "Methods", "Results" all go into sections.format="pdf" — because it comes after *sections, it is keyword-only. It must be passed by name. The call uses format="html".**metadata — collects any remaining keyword values into a dict. confidential=True goes here.
🎯 Positional-only parameters — the / marker (Python 3.8+)
Everything before the / in a function signature is positional-only. This is useful when the parameter name is an implementation detail you don't want callers to depend on, or when the function is a wrapper around a C function that doesn't recognise keyword arguments. The name title is visible inside the function, but callers cannot write create_report(title="...").
def to_power(base, exponent, /): return base ** exponent print(to_power(2, 10)) try: to_power(base=2, exponent=10) except TypeError as e: print(f"Error: {e}")
base and exponent are before /, so both are positional-only.Line 4 —
to_power(2, 10) works and returns 1024.Lines 6–9 — calling with keyword arguments raises
TypeError: to_power() got some positional-only arguments passed as keyword arguments: 'base, exponent'.
🔒 Keyword-only parameters — the bare * marker
A bare * in a function signature forces every parameter after it to be keyword-only. This is useful when a boolean flag would be ambiguous as a positional argument — save(data, True) is unclear, but save(data, compress=True) is not.
def save_file(content, *, compress=False, backup=True): print(f"Saving {len(content)} chars | compress={compress} | backup={backup}") save_file("hello world") save_file("hello world", compress=True) try: save_file("hello", True) except TypeError as e: print(f"Error: {e}")
* marks the boundary. compress and backup are keyword-only.Line 4 —
save_file("hello world") uses both defaults.Line 5 —
compress=True overrides one default; backup keeps its default.Lines 7–10 —
save_file("hello", True) raises TypeError: save_file() takes 1 positional argument but 2 were given. The True cannot be assigned to compress positionally.
save_file(data, compress=True) they don't need to check the function definition to understand what True means. This is why many modern Python libraries mark boolean flags as keyword-only.7️⃣ Unpacking — Spreading Collections into Arguments
The flip side of *args and **kwargs: spreading them back out
At the call site, * and ** do the reverse of what they do in a definition. *iterable spreads the elements of a list or tuple into positional arguments. **mapping spreads the key–value pairs of a dictionary into keyword arguments. This is how you call a function when your arguments are already in a collection.
📝 Example — unpacking a list and a dictionary
def make_pizza(size, *toppings, crust="regular"): print(f"Size: {size}") print(f"Toppings: {toppings}") print(f"Crust: {crust}") args_list = ["large", "cheese", "mushrooms"] kwargs_dict = {"crust": "thin"} make_pizza(*args_list, **kwargs_dict)
args_list holds positional values. *args_list spreads them into the call: "large" fills size; "cheese" and "mushrooms" go into toppings.Line 7 —
kwargs_dict holds a keyword value. **kwargs_dict spreads it: crust="thin".Line 9 — the call is equivalent to
make_pizza("large", "cheese", "mushrooms", crust="thin").
📦 Unpacking in literals (Python 3.5+)
The same * and ** syntax works inside list, tuple, set, and dict literals to merge collections inline.
nums1 = [1, 2, 3] nums2 = [4, 5, 6] combined = [*nums1, *nums2, 7, 8] print(combined) dict1 = {"a": 1, "b": 2} dict2 = {"c": 3, "d": 4} merged = {**dict1, **dict2, "e": 5} print(merged)
[*nums1, *nums2, 7, 8] spreads both lists into a new list, then adds the extra values. Result: [1, 2, 3, 4, 5, 6, 7, 8].Line 8 —
{**dict1, **dict2, "e": 5} merges both dictionaries, then adds a new key. If keys collided, the later value wins — here there's no collision.
⚠️ Common error — unpacking the wrong type
❌ Dict with * instead of **
func(*{"a": 1}) spreads the keys, not the values. This is rarely what you want and often produces a confusing TypeError.
✅ Dict with **
func(**{"a": 1}) passes a=1 as a keyword argument. This is the correct pairing: * for sequences, ** for mappings.
The rule is simple: * is for iterables (lists, tuples, generators); ** is for mappings (dicts, Mapping objects). Mixing them up is a common source of subtle bugs.
📏 The Argument Order Rules — One Reference Table
Every rule you need, in one place
Python enforces a fixed order for the parts of a function signature. This table summarises every rule, with a short example for each. If you remember this table, you can read or write any function signature correctly.
| Rule | What it means | Example |
|---|---|---|
| Positional before keyword | In a call, all positional arguments must come before any keyword arguments. | f(1, 2, x=3) |
| Default after required | A parameter with a default cannot precede a parameter without one. | def f(a, b=1) ✅def f(a=1, b) ❌ |
| Positional-only boundary | A / marks the end of positional-only parameters. Callers cannot pass these by name. |
def f(a, /, b) |
| Keyword-only boundary | A bare * (or *args) marks the start of keyword-only parameters. Callers must pass these by name. |
def f(a, *, b) |
*args position |
*args can appear after all positional parameters, but before keyword-only parameters. |
def f(a, *args, b) |
**kwargs position |
**kwargs must be the very last item in the parameter list. |
def f(a, *args, b, **kwargs) |
| No duplicate parameters | Parameter names must be unique — each parameter can only appear once. | def f(a, a) ❌ |
🧠 The canonical order (memorise this)
📋 Quick-Reference Cheat Sheet
Every syntax form at a glance — bookmark this page
✅ Definition vs. call — the two sides of * and **
In a function definition
*args — collect positional values into a tuple
**kwargs — collect keyword values into a dict
In a function call
*iterable — spread a sequence into positional values
**mapping — spread a dict into keyword values
🔄 All argument kinds, one example each
def demo(pos_only, /, normal, *args, kw_only, **kwargs): print(f"pos_only: {pos_only}") print(f"normal: {normal}") print(f"args: {args}") print(f"kw_only: {kw_only}") print(f"kwargs: {kwargs}") demo("hello", "world", 1, 2, 3, kw_only="yes", extra=True)
"hello" → pos_only (positional-only, first argument)"world" → normal (positional-or-keyword)1, 2, 3 → collected into args as (1, 2, 3)kw_only="yes" → kw_only (keyword-only, required)extra=True → kwargs as {'extra': True}
⚠️ Common errors and what they mean
print("TypeError: missing 1 required positional argument") print(" → You didn't pass a required positional value.") print("TypeError: got an unexpected keyword argument") print(" → You passed a keyword argument that doesn't match any parameter name.") print("TypeError: got multiple values for argument") print(" → The same parameter was filled positionally AND by keyword.") print("TypeError: takes 2 positional arguments but 3 were given") print(" → You passed too many positional values (and there's no *args to catch them).") print("TypeError: missing 1 required keyword-only argument") print(" → A parameter after * or *args was not passed by name.")
What to remember from this lesson
1. Positional arguments are matched by order; keyword arguments are matched by name.
2. Default values make parameters optional — but never use a mutable default.
3. *args collects extra positional values into a tuple; **kwargs collects extra keyword values into a dict.
4. At call time, * spreads a sequence and ** spreads a dict.
5. The canonical signature order is: positional-only, positional-or-keyword, *args, keyword-only, **kwargs.