# How Python Handles Unknown Arguments: A Deep Dive into *args and **kwargs

If you've spent any time reading Python code, you've probably come across function signatures that look like this:

```python
def some_function(*args, **kwargs):
    pass
```

And wondered, what are those asterisks doing there? What are `*args` and `**kwargs`? Why does every Python codebase seem to have them?

By the end of the article, you'll have a complete understanding of what they are, how they work, and more importantly **why** they exist. We'll build up from the problem they solve, through the mechanics, all the way to a real-world decorator you can use in your own projects.

* * *

## Before We Begin: Two Types of Arguments

Python functions accepts two distinct types of arguments. Understanding the difference is essential before diving into `*args` and `**kwargs`.

### Positional Arguments

A positional argument is passed by **position**. In other words, its meaning is determined by where it appears in the function call:

```python
def create_user(name, age, city):
    print(f"{name}, {age}, {city}")

create_user("Moussa", 27, "Porto-Novo")
# Moussa, 27, Porto-Novo
```

Here, `"Moussa"` maps to `name`, `27` maps to `age`, and `Porto-Novo` maps to `city`, purely based on their **order**,

Swap them and the meaning changes entirely:

```python
create_user("Porto-Novo", 27, "Moussa")
# Porto-Novo, 27, Moussa (WRONG)
```

### Keyword Arguments

A keyword argument is a passed by **name**. You explicitly state which parameter receives which value, so order no longer matters:

```python
create_user(city="Porto-Novo", name="Moussa", age=27)
# Moussa, 27, Porto-Novo (correct, despite different order)
```

The same function accepts both styles. Python handles they differently under the hook, and that distinction is exactly what `*args` and `**kwargs` are built on.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Parameter</strong> is the variable name defined in the function signature. <strong>Argument</strong> is the actual value passed when calling the function.</div>
</div>

```python
def greet(name):   # 'name' is a PARAMETER
    print(name)

greet("Moussa")    # "Moussa" is an ARGUMENT
```

* * *

## The Problem: Functions With Fixed Arguments

Let's start with something familiar. Here's a basic Python function:

```python
def add(a, b):
    return a + b

print(add(1, 2))  # 3
```

This works perfectly, as long as you always have exactly two numbers to add. But what if you want to add three? Or five? Or you don't know in advance how many you'll have?

```python
print(add(1, 2, 3))
# TypeError: add() takes 2 positional arguments but 3 were given
```

Python refuses. The function signature is a "contract": exactly two arguments. No more, no less.

You could try to work around it by writing separate functions:

```python
def add_two(a, b):
    return a + b

def add_three(a, b, c):
    return a + b + c
```

But this obviously doesn't scale. You'd need a new function for every possible number of arguments. There has to be a better way. Don't you think so 😀? That's exactly what `*args` was designed to solve.

* * *

## Part 1: `*args` - Collecting Unlimited Positional Arguments

Here's the solution:

```python
def add(*args):
    return sum(args)

print(add(1, 2))           # 3
print(add(1, 2, 3))        # 6
print(add(1, 2, 3, 4, 5))  # 15
```

The `*` in the function signature tells Python: ***collect all positional arguments into a single tuple***. Inside the function, `args` is just a regular tuple. You can iterate it, index it, pass it to `sum()`, or anything else you'd do with a normal tuple.

### The `*` Is the Mechanism, Not the Name

This is important: the `*` is what does the work, not the word `args`. You can call it anything:

```python
def add(*numbers):
    return sum(numbers)

def add(*values):
    return sum(values)
```

Both work identically. The name `args` is a widely adopted convention. You'll see it in almost every Python codebase, but it's just a name.

### Mixing Fixed Arguments With `*args`

You can combine fixed arguments with `args`. The fixed ones must come first:

```python
def greet(greeting, *names):
    for name in names:
        print(f"{greeting}, {name}!")

greet("Hello", "Fabien", "Yves", "John")
# Hello, Fabien!
# Hello, Yves!
# Hello, John!
```

`greeting` captures the first argument, and `*names` collects everything else into a tuple.

### Common Mistake: Putting `*args` Before Fixed Arguments

```python
def greet(*names, greeting):
    pass

greet("Alice", "Bob", "Hello")
# TypeError: greet() missing 1 required keyword-only argument: 'greeting'
```

When `*args` appears before a named parameter, that named parameter becomes ***keyword-only***. As a result, it must be passed by **name**, not position. This is a common source of confusion. Keep fixed positional arguments before `*args` and you'll avoid it.

### Mental Model

> `*args` **\= "Pack all positional arguments into a tuple called** `args`**".**

* * *

## Part 2: `**kwargs` - Collecting Unlimited Keyword Arguments

Python functions also accept ***keyword arguments***, arguments passed by name:

```python
def create_user(name, age, city):
    print(f"{name}, {age}, {city}")

create_user(name="Moussa", age=27, city="Porto-Novo")
```

What if you don't know which keyword argument will be passed in advance? That's where `**kwargs` comes in:

```python
def create_user(**kwargs):
    print(kwargs)

create_user(name="Moussa", age=27, city="Porto-Novo")
# {'name': 'Moussa', 'age': 27, 'city': 'Porto-Novo'}
```

The `**` tells Python: ***collect all keyword arguments into a dictionary***. The argument names become keys, and the values you pass become the dictionary values.

Inside the function, `kwargs` is just a regular dictionary:

```python
def create_user(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

create_user(name="Moussa", age=27, city="Porto-Novo")
# name: Moussa
# age: 27
# city: Porto-Novo
```

### Accessing Specific Keys Safely

When you know which keys you're looking for, use `.get()` with a default value:

```python
def create_server(**kwargs):
    host = kwargs.get("host", "localhost")
    port = kwargs.get("port", 8000)
    debug = kwargs.get("debug", False)
    print(f"Starting {host}:{port} | debug={debug}")

create_server(port=3000)
# Starting localhost:3000 | debug=False
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><code>.get()</code> safely returns the default value if a key is missing, rather than throwing a <code>KeyError</code>. Note that the default value is the second argument you pass to <code>.get()</code></div>
</div>

### Mental Model

> `**kwargs` = "Pack all keyword arguments into a dictionary called `kwargs`

Same pattern as `*args`, but with a different container:

*   `*args`: Positional arguments **(tuple)**
    
*   `**kwargs`: Keyword arguments **(dict)**
    

* * *

## Part 3: The `*` Unpack Direction - and `**` at the Call Site

Here's the part that trips most people up: the `*` *and* `**` operators work in *both directions.*

*   In a **function definition**, they *pack* arguments in
    
*   In a **function call**, they *unpack* a collection out
    

```python
 def add(a, b, c):
    return a + b + c

numbers = [1, 2, 3]
print(add(*numbers))  # ✅ Unpacks the list into positional arguments → 6

details = {"a": 1, "b": 2, "c": 3}
print(add(**details))  # ✅ Unpacks the dict into keyword arguments → 6
```

The `*` doesn't care whether you give it a list or a tuple. It works on any [iterable](https://www.w3schools.com/python/python_iterators.asp):

```python
my_tuple = (1, 2, 3)
my_list = [1, 2, 3]

print(add(*my_tuple))  # ✅ 6
print(add(*my_list))   # ✅ 6
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">An iterable is any object Python can loop over. Under the hook, it is simply an object that implements the <code>__iter__()</code>. method, which returns an <strong>iterator </strong>that yields values one at a time.</div>
</div>

The `**` operator only works on mappings (objects with key-value pairs). For example, a `set` has no keys, so Python has no idea which value maps to which argument:

```python
my_set = {"Moussa", "Porto-Novo"}
some_function(**my_set)  # ❌ TypeError — sets have no keys
```

### The Mental Model

| Syntax | Where | What it does | Result |
| --- | --- | --- | --- |
| `*args` | Definition | Packs positional args | Tuple |
| `**kwargs` | Definition | Packs keyword args | Dict |
| `*iterable` | Call | Unpacks into positional args | Spread |
| `**dict` | Call | Unpacks into keyword args | Spread |

Same symbol, opposite direction. Once you internalize the table above, the behavior becomes predictable everywhere you encounter it.

* * *

## Part 4: Combining `*args` and `**kwargs`

You can use both in the same function. Python automatically routes each argument to the right place. No name (position) argument attached goes to `args`, name argument attached goes to `kwargs`:

```python
def describe(*args, **kwargs):
    print("Positional:", args)
    print("Keyword:", kwargs)

describe(1, 2, 3, name="Moussa", city="Porto-Novo")
# Positional: (1, 2, 3)
# Keyword: {'name': 'Moussa', 'city': 'Porto-Novo'}
```

### The Order Rule

When combining everything, Python enforces a strict order:

```python
def func(fixed, *args, **kwargs):
    pass
```

1.  Fixed positional arguments first
    
2.  `*args` second
    
3.  `**kwargs` last
    

Breaking this order produces an immediate `SyntaxError` and Python won't even run the file.

### Empty Defaults

When no arguments are passed, `*args` and `**kwargs` don't become `None`. They become their empty defaults:

```python
def safe_summary(*args, **kwargs):
    print(args)   # ()  ← empty tuple
    print(kwargs) # {}  ← empty dict

safe_summary()
```

This matters because code like `for item in args` still works safely with no arguments. `None` would crash it.

* * *

## Part 5: The Wrapper Pattern - Where This Gets Powerful

The most important real-world use of `*args` and `**kwargs` I know is the **wrapper pattern**: forwarding arguments through a function without caring what they are.

Here is the code idea:

```python
def log_call(func, *args, **kwargs):
    print(f"Calling {func.__name__}")
    result = func(*args, **kwargs)
    print(f"Done")
    return result

def add(a, b):
    return a + b

def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

log_call(add, 1, 2)
log_call(greet, "Moussa", greeting="Hey")
```

`log_call` doesn't know or care what `add` or `greet` need. It collects everything with `*args`, `**kwargs` and passed it straight through. The structure is always the same:  
**Collect** --> **Do something** --> **Forward**

* * *

## Part 6: Building a Real Decorator

The wrapper pattern is the foundation of Python decorators. A decorator is a function that takes a function, wraps it with extra behavior, and returns the wrapped version.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You can learn more about the concept of decorators in Python on my blog. I have a <a target="_blank" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="https://blog.moussaamzat.dev/series/python-decorators" style="pointer-events: none;">comprehensive series</a> that teaches you everything from the ground up.</div>
</div>

Here's a `@timed` decorator that measures how long any function takes to run:

```python
import time

def timed(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        elapsed_time = (time.time() - start_tim) * 1000
        print(f"Ran {func.__name__} in ~{elapsed_time:.0f}ms")
        return result
    return wrapper

@timed
def slow_add(a, b):
    time.sleep(0.1)
    return a + b

@timed
def greet(name, greeting="Hello"):
    time.sleep(0.05)
    return f"{greeting}, {name}!"

result = slow_add(1, 2)
print(result)
# Ran slow_add in ~100ms
# 3

result = greet("Moussa", greeting="Hey")
print(result)
# Ran greet in ~50ms
# Hey, Moussa!
```

Notice that `wrapper` uses `*args`, and `**kwargs` to collect and forward arguments, making `@timed` reusable across *any* function regardless of its signature. This is exactly how decorators work in Flask and FastAPI.

* * *

## Key Takeaways

*   `*args` collects unlimited **positional** arguments into a **tuple**
    
*   `**kwargs` collects unlimited **keyword** arguments into a **dict**
    
*   Inside the function, both are just normal Python data structures
    
*   The correct order is always: `fixed -> *args -> **kwargs`
    
*   Empty `*args` is `()` and empty `**kwargs` is `{}`, never `None`
    
*   `*` and `**` work in both directions: packing in definitions, unpacking at call sites
    
*   The wrapper pattern: **collect**, **act**, **forward** is the foundation of decorators.
    

Once these rules are internalized, you'll recognize the pattern everywhere: in the standard library, in popular frameworks, and in well-written Python code across the ecosystem.

Thank you for reading 📖🙂.
