*args and **kwargs in Python

The previous article touched on *args and **kwargs briefly while covering the full parameter-ordering rules — this one is dedicated entirely to python *args **kwargs, covering exactly how each works, how to unpack existing collections into function calls, and where this pattern genuinely earns its place versus where it just adds unnecessary complexity.

Introduction: The Problem *args and **kwargs Solve

The pain point without them

Imagine writing a function that adds numbers together, but you don't know in advance how many numbers the caller will want to add. Without a mechanism for variable-length arguments, you'd be stuck writing something impractical:

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

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

def add_four(a, b, c, d):
    return a + b + c + d
# ...and so on, forever

That obviously doesn't scale. *args and **kwargs exist specifically to solve this: accepting any number of arguments through a single, flexible parameter.

The quick framing

*args packs any extra positional arguments into a tuple. **kwargs packs any extra keyword arguments into a dictionary.

A naming clarification worth getting right

Here's something worth being precise about early: args and kwargs are just conventional names — Python doesn't require those exact words. What actually matters, syntactically, is the * and ** operators in front of them. You could technically write *numbers or **options instead, and it would work identically. That said, *args and **kwargs are such a strong, universal convention across the Python ecosystem that deviating from them without a good reason (like *numbers genuinely being more descriptive in a specific context) tends to make code slightly less immediately recognizable to other Python developers.

*args: Variable-Length Positional Arguments

Basic syntax and example
def sum_all(*args):
    return sum(args)

print(sum_all(1, 2))            # 3
print(sum_all(1, 2, 3, 4, 5))   # 15
print(sum_all())                 # 0 — args is just an empty tuple

Inside the function, args is a regular tuple, built automatically from however many positional arguments the caller actually supplied. You can loop over it, index into it, or pass it to sum() exactly like any other tuple.

Combining *args with regular positional parameters

You can mix explicit, named positional parameters with *args — the named ones fill first, and anything left over gets packed into args:

def describe_trip(destination, *stops):
    print(f"Destination: {destination}")
    for stop in stops:
        print(f"  Stop: {stop}")

describe_trip("Paris", "London", "Amsterdam", "Berlin")
# Destination: Paris
#   Stop: London
#   Stop: Amsterdam
#   Stop: Berlin

destination claims the first positional argument, and everything after it gets swept into stops as a tuple.

Unpacking a list or tuple into separate arguments

The * operator works in the opposite direction too — at call time, prefixing an existing list or tuple with * unpacks it into separate positional arguments:

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

numbers = [1, 2, 3]
print(add(*numbers))   # 6 — equivalent to add(1, 2, 3)

Without the *, add(numbers) would try to pass the entire list as a single argument, which would fail since add() expects three separate values. *numbers spreads the list's contents out into individual arguments instead.

The error when the count doesn't match

If the unpacked collection doesn't have exactly the right number of items for the function's positional parameters, Python raises an error immediately:

numbers = [1, 2]
print(add(*numbers))
# TypeError: add() missing 1 required positional argument: 'c'

**kwargs: Variable-Length Keyword Arguments

Basic syntax and example
def print_settings(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_settings(timeout=5, retries=3, debug=True)
# timeout: 5
# retries: 3
# debug: True

Inside the function, kwargs is a regular dictionary, built from whatever named arguments the caller happened to pass — no fixed set of expected keyword names required in the function definition itself.

Unpacking a dictionary into keyword arguments

The ** operator, like *, works in reverse at call time — prefixing an existing dictionary with ** unpacks its key-value pairs into separate keyword arguments:

def create_user(name, email, role):
    print(f"{name} ({email}) — {role}")

user_data = {"name": "Alex", "email": "[email protected]", "role": "admin"}
create_user(**user_data)
# Alex ([email protected]) — admin

This is a genuinely common pattern when working with data that's already shaped as a dictionary — say, a parsed JSON object or a database record — and you need to pass its fields into a function expecting individual keyword arguments.

Practical pattern: kwargs.get() for flexible configuration

Inside a function accepting **kwargs, .get() (covered in the earlier dictionaries article) is the natural way to check for specific optional settings, with a sensible fallback if the caller didn't provide them:

def connect(**kwargs):
    timeout = kwargs.get("timeout", 30)
    retries = kwargs.get("retries", 3)
    print(f"Connecting with timeout={timeout}, retries={retries}")

connect()                              # Connecting with timeout=30, retries=3
connect(timeout=10)                    # Connecting with timeout=10, retries=3
connect(timeout=10, retries=5)         # Connecting with timeout=10, retries=5

This gives callers the flexibility to override only the specific settings they care about, while everything else quietly falls back to a reasonable default.

Combining Both, and the Strict Ordering Rules

Legal signature order

As covered in the previous article on function arguments, Python enforces a strict order when a function signature combines several parameter types: standard positional parameters*argskeyword-only parameters**kwargs.

def example(a, b, *args, c=10, **kwargs):
    print(a, b, args, c, kwargs)

example(1, 2, 3, 4, c=99, extra="hello")
# 1 2 (3, 4) 99 {'extra': 'hello'}

Walking through that call: a and b claim the first two positional arguments, 3 and 4 get swept into args as a tuple (since there are no more named positional parameters to fill), c is explicitly overridden via keyword, and extra="hello" — not matching any named parameter — gets packed into kwargs.

Why reversing the order raises a SyntaxError
def broken(**kwargs, *args):   # SyntaxError — wrong order
    pass

**kwargs has to come last in a function definition, because it's designed to catch everything else that wasn't matched by anything defined before it — there's nothing meaningful left for Python to place after it. Similarly, *args needs to come before any keyword-only parameters, since those keyword-only parameters rely on *args having already "used up" its share of the positional arguments before they're addressed by name.

Real-World Use Cases and When to Avoid Them

Wrapper and decorator functions
This is arguably the single most common legitimate use of *args/**kwargs together: writing a function that wraps another function, without needing to know or duplicate that other function's exact parameter list.
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"{func.__name__} took {elapsed:.4f} seconds")
        return result
    return wrapper

@timer
def slow_add(a, b):
    time.sleep(1)
    return a + b

slow_add(3, 5)
# slow_add took 1.0002 seconds

Here, wrapper(*args, **kwargs) can forward any combination of arguments through to func, regardless of what func's actual signature looks like — a genuinely essential pattern for decorators (a topic covered in more depth in a later article in this series), logging wrappers, timing utilities, and caching layers, all of which need to transparently pass arguments through to whatever function they're wrapping.

Framework and library patterns

You'll also see *args/**kwargs used extensively in libraries and frameworks that need to pass extra, optional configuration through multiple layers of function calls without every intermediate layer needing to know about every possible option in advance. This lets a library add new configuration options over time without breaking the signatures of functions that merely pass those options along.

Honest guidance: don't overuse this

It's worth being direct about a real tradeoff here: *args/**kwargs genuinely reduce readability and hurt IDE autocompletion compared to explicit, named parameters. When you call a function defined with plain named parameters, your editor can tell you exactly what's expected and catch a typo in a keyword name immediately. When a function just accepts **kwargs, that safety net disappears — the editor has no way to know what keys are actually meaningful, and a typo in a keyword argument silently gets swallowed into the kwargs dictionary rather than raising a helpful error.

The practical rule of thumb: use explicit, named parameters whenever the set of arguments a function needs is fixed, known, and reasonably small — which covers the overwhelming majority of functions you'll write. Reach for *args/**kwargs specifically when you need genuine flexibility — an unknown or highly variable number of arguments, or a wrapper function forwarding arguments to something else without needing to inspect them. Using **kwargs as a substitute for just writing out a function's actual expected parameters, purely to "future-proof" it or avoid deciding on a signature, tends to produce code that's harder to use correctly, not more flexible in any way that matters.