Historical Context & Motivation
Imagine you are building a function that adds numbers together. You might start by writing a function that takes two numbers, but what happens when you need to add three, five, or even twenty numbers? You would have to write a brand-new function every time, or keep adding more and more parameters. That gets messy fast. Python's creators recognized this problem early on and built a powerful feature directly into the language: variable-length arguments, commonly known as *args and **kwargs.
The idea of passing a variable number of arguments to a function did not originate with Python. Older languages like C had similar mechanisms (the ... ellipsis notation), but they were error-prone and hard to use. Python's approach, refined over decades of development, made variable arguments both safe and elegant. Let's trace a few key moments in this evolution.
... for variable argument lists, but with no type safety and a clunky macro-based interface.*args for positional variable arguments, making flexible functions far easier to write.**kwargs, enabling functions to accept any number of keyword arguments as a dictionary.* separator, building on the same packing mechanism.The core question this lesson addresses is simple but important: How do you write a single function that can handle any number of inputs, without knowing in advance how many there will be? Python's answer — *args and **kwargs — is one of the most useful tools in any Python programmer's toolkit.
Core Principles & Definitions
Before diving into code, let's nail down the key ideas. The single asterisk * and double asterisk ** in front of a parameter name are not just decoration — they tell Python to pack extra arguments into a single container. The names args and kwargs are just conventions — you could call them *stuff and **options if you wanted. It's the asterisks that do the magic.
*args — Positional Packing
*args parameter collects all extra positional arguments into a tuple. You can then loop through them or index into them just like any tuple.**kwargs — Keyword Packing
**kwargs parameter collects all extra keyword arguments into a dictionary. Each key is the argument name, and each value is the argument's value.Parameter Order Matters
Unpacking: The Reverse
* and ** when calling a function to unpack a list or dictionary into separate arguments.*args like a backpack: you can toss in as many items (arguments) as you want, and Python zips them up into one neat bundle (a tuple). **kwargs is like a labeled filing cabinet: each item goes in with a name tag (keyword), and Python organizes them into a dictionary you can look up by name.Visual Explanation
The diagram below shows exactly what happens when Python encounters a function call with extra arguments. On the left you see the arguments being passed in; on the right you see how Python packs them into the args tuple and kwargs dictionary inside the function body.
msg being assigned directly. The violet box shows extra positional arguments packed into the args tuple. The pink box shows extra keyword arguments packed into the kwargs dictionary.Notice that the regular parameter msg consumes the first positional argument, and only the leftover positional arguments end up in args. Similarly, only the leftover keyword arguments — those that do not match any named parameter — end up in kwargs. This is why order matters in the function definition.
How It Works Under the Hood
Packing: From Arguments to Containers
When Python sees a * in a function definition, it enters a special "packing mode." After all regular positional parameters have been satisfied, any remaining positional arguments are gathered into a tuple. A tuple is an immutable (unchangeable) sequence — think of it as a read-only list. Python chose a tuple instead of a list to signal that these packed values should not be modified inside the function.
a and b consume positions 0 and 1. Everything from position 2 onward is packed into args.Similarly, the ** operator collects leftover keyword arguments into a dictionary. A dictionary maps keys (the keyword names, which are strings) to values. This lets you access the extra arguments by name instead of by position.
a gets added to the kwargs dictionary.Unpacking: From Containers to Arguments
The same * and ** operators can work in reverse when you call a function. If you have a list nums = [1, 2, 3] and you call add(*nums), Python unpacks the list so the function receives add(1, 2, 3). For dictionaries, **my_dict unpacks key-value pairs into keyword arguments.
* "spreads" a list or tuple, while ** "spreads" a dictionary into keyword arguments.Parameter Order & Classification
Python enforces a strict order for parameters in a function definition. Getting this order wrong causes a SyntaxError. The diagram below illustrates the required order from left to right, along with the data type each parameter produces inside the function.
*args (violet), then keyword-only params (amber), and finally **kwargs (pink). The bottom shows a realistic example with all four types.| Parameter Type | Symbol | Data Type Inside Function | Example Values |
|---|---|---|---|
| Regular positional | a | Whatever type is passed | a = 5 |
| Default value | b=10 | Whatever type is passed (or default) | b = 10 |
| *args | *args | tuple | (2, 3, 4) |
| Keyword-only | sep="-" | Whatever type is passed | sep = "-" |
| **kwargs | **kwargs | dict | {"x": 1, "y": 2} |
Worked Example: Building a Flexible Logger
Let's build a practical logging function that accepts a log level, any number of messages, and optional metadata like a timestamp or user ID. This is the kind of function you would actually use in a real project.
level (like "INFO" or "ERROR"), then *messages for any number of log messages, and **metadata for optional extra info.def log(level, *messages, **metadata):messages is a tuple. We loop through it and join all the messages into one string separated by spaces.full_msg = " ".join(messages)metadata dictionary might contain keys like user or timestamp. We loop through the key-value pairs to build a detail string.details = ", ".join(f"{k}={v}" for k, v in metadata.items())print(f"[{level}] {full_msg} | {details}")log with different numbers of arguments each time. Notice how the same function handles all these cases without any changes.log("INFO", "Server started") → [INFO] Server started |
log("ERROR", "File", "not", "found", user="alice", code=404)
→ [ERROR] File not found | user=alice, code=404*args receives no extra arguments, it becomes an empty tuple (). If **kwargs receives no extra keywords, it becomes an empty dictionary {}. Your code should handle these empty cases gracefully — the function won't crash, it just has nothing extra to process.Strengths, Limitations & When to Use Each
Both *args and **kwargs make your functions more flexible, but they are not interchangeable. Each shines in different situations. Let's compare them side by side.
| Feature | *args | **kwargs |
|---|---|---|
| Collects | Extra positional arguments | Extra keyword arguments |
| Data type | tuple | dict |
| Access by | Index: args[0] | Key: kwargs["name"] |
| Best for | Lists of similar items (numbers, names, paths) | Configuration options with descriptive names |
| Limitation | No labeling — you must remember order | Slightly more verbose to call |
| Common use case | sum_all(1, 2, 3, 4) | connect(host="...", port=80) |
*args when your function processes a variable number of similar items — like adding numbers or joining strings. Use **kwargs when your function accepts optional settings or configuration where each argument has a meaningful name. Use both together when you want maximum flexibility — like a function that can handle any combination of inputs and options.Connection to Advanced Python Patterns
Once you're comfortable with *args and **kwargs, you'll find them everywhere in real Python code. They are the foundation of several advanced patterns that you'll encounter as you keep learning.
| Intro Concept | Advanced Pattern | Why It Matters |
|---|---|---|
*args in your own functions | Decorators — wrapper functions that modify other functions | Decorators use *args, **kwargs to wrap any function regardless of its signature. |
**kwargs for optional settings | Class inheritance — passing constructor args up the chain | Child classes use super().__init__(**kwargs) to forward arguments to parent classes. |
Unpacking with *list | Functional programming — map(), zip() | Functions like zip(*matrix) use unpacking to transpose data structures. |
Combining *args + **kwargs | API design — building frameworks and libraries | Libraries like Flask and Django heavily use **kwargs for flexible configuration. |
Don't worry about mastering these advanced patterns right now. The key takeaway is that *args and **kwargs are not just a beginner concept you'll outgrow — they are a fundamental building block that professional Python developers use every day. Learning them now prepares you for decorators, class hierarchies, and framework development later on.
Practice Problems
*args and **kwargs in your own words. What data type does each produce inside the function, and what kind of arguments does each collect?multiply_all that uses *args to accept any number of numbers and returns their product. For example, multiply_all(2, 3, 4) should return 24.build_profile(name, *hobbies, **info) that returns a dictionary with the key "name" set to the name, "hobbies" set to a list of hobbies, and all the keyword arguments merged in. Calling build_profile("Maya", "art", "coding", age=15, grade="10th") should return {"name": "Maya", "hobbies": ["art", "coding"], "age": 15, "grade": "10th"}.make_tag(tag, *content, **attributes) that produces HTML. For example, make_tag("p", "Hello", "World", class_="intro", id="greeting") should return the string '<p class="intro" id="greeting">Hello World</p>'. Hint: join content with a space and loop through attributes.items() to build the attribute string. Use k.rstrip("_") to strip trailing underscores from attribute names (since class is a reserved word in Python).def mystery(a, b, *args, mode="add", **kwargs):. For the call mystery(1, 2, 3, 4, mode="multiply", x=10, y=20), state the exact value of each parameter inside the function: a, b, args, mode, and kwargs. Then explain why mode is NOT captured by **kwargs even though it is passed as a keyword argument.Lesson Summary
Python's *args syntax collects extra positional arguments into a tuple, while **kwargs collects extra keyword arguments into a dictionary. The names args and kwargs are conventions — the single asterisk (*) and double asterisk (**) are what trigger the packing behavior. Together they let you write a single flexible function that handles any combination of inputs.
The required parameter order in a function signature is: regular parameters → *args → keyword-only parameters → **kwargs. You can also use * and ** when calling a function to unpack a list or dictionary into individual arguments. These tools form the foundation of advanced Python patterns including decorators, class inheritance, and API design.