I remember writing functions early in my Python journey that could only accept a fixed number of parameters, and running into walls whenever I needed to handle a variable amount of input — sometimes two arguments, sometimes five, sometimes none. Once I learned *args and **kwargs, that entire category of problem disappeared. In this guide, I’ll cover exactly how these work internally, when to use each, and the patterns I rely on daily.
The Core Idea
*args and **kwargs let a function accept an arbitrary number of positional and keyword arguments, respectively. The names args and kwargs are just convention — the actual mechanism is the * and ** operators, which I could technically pair with any valid variable name.
def demo(*args, **kwargs):
print("Positional args:", args)
print("Keyword args:", kwargs)
demo(1, 2, 3, name="Alice", age=30)
Output:
Positional args: (1, 2, 3)
Keyword args: {'name': 'Alice', 'age': 30}
How *args Works
The single asterisk collects any extra positional arguments into a tuple. Inside the function, args behaves like any other tuple — I can iterate over it, index into it, or unpack it further.
def total(*args):
return sum(args)
print(total(1, 2, 3)) # 6
print(total(10, 20)) # 30
print(total()) # 0
I can also mix *args with regular positional parameters, as long as *args comes after them:
def greet(greeting, *names):
for name in names:
print(f"{greeting}, {name}!")
greet("Hello", "Alice", "Bob", "Carol")
Output:
Hello, Alice!
Hello, Bob!
Hello, Carol!
How **kwargs Works
The double asterisk collects any extra keyword arguments into a dictionary, where the keys are the argument names (as strings) and the values are whatever was passed in.
def print_profile(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_profile(name="Alice", age=30, city="Boston")
Output:
name: Alice
age: 30
city: Boston
Combining Regular Parameters, *args, and **kwargs
Python enforces a strict ordering for how these can appear together in a function signature:
def func(positional, *args, keyword_only=None, **kwargs):
...
The order must always be:
- Standard positional parameters
*args- Keyword-only parameters (parameters that come after
*argsand must be passed by name) **kwargs
Here’s a realistic example combining all of them:
def build_request(url, *args, method="GET", **headers):
print("URL:", url)
print("Extra positional args:", args)
print("Method:", method)
print("Headers:", headers)
build_request("https://api.example.com", "extra1", method="POST", Authorization="Bearer xyz")
Output:
URL: https://api.example.com
Extra positional args: ('extra1',)
Method: POST
Headers: {'Authorization': 'Bearer xyz'}
Unpacking Arguments When Calling a Function
* and ** aren’t just for defining functions — I use them just as often for calling functions, to unpack a list or dictionary into individual arguments.
def add(a, b, c):
return a + b + c
numbers = [1, 2, 3]
print(add(*numbers)) # unpacks to add(1, 2, 3) -> 6
params = {"a": 10, "b": 20, "c": 30}
print(add(**params)) # unpacks to add(a=10, b=20, c=30) -> 60
This is incredibly useful when the data I have is already structured as a list or dict and I need to feed it into a function that expects individual arguments.
Why I Use *args and **kwargs: Real-World Patterns
Writing Flexible Wrapper Functions and Decorators
This is, hands down, the most important real-world use case I run into. When writing a decorator, I don’t know in advance what arguments the wrapped function will take — *args and **kwargs let the wrapper forward anything through to the original function.
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_add(a, b):
time.sleep(0.1)
return a + b
print(slow_add(3, 4))
Without *args, **kwargs, I’d have to write a separate wrapper for every possible function signature, which obviously doesn’t scale.
Subclassing and Passing Arguments to super().__init__()
I use this constantly when extending existing classes, especially in frameworks like Django or when working with class hierarchies I don’t fully control:
class Animal:
def __init__(self, name, sound):
self.name = name
self.sound = sound
class Dog(Animal):
def __init__(self, *args, breed, **kwargs):
super().__init__(*args, **kwargs)
self.breed = breed
d = Dog("Rex", "Woof", breed="Labrador")
print(d.name, d.sound, d.breed)
Building Flexible APIs
When designing a utility function meant to be used in many different contexts, **kwargs gives me a way to accept optional configuration without bloating the function signature with a dozen optional parameters.
def create_chart(data, **options):
title = options.get("title", "Untitled Chart")
color = options.get("color", "blue")
print(f"Creating '{title}' chart in {color} using {len(data)} points")
create_chart([1, 2, 3], title="Sales", color="green")
create_chart([4, 5, 6])
Internals: How Python Handles This at the Bytecode Level
When Python parses a function call, it separates the arguments into positional and keyword groups before binding them to the function’s parameters. For a function defined with *args, any positional arguments beyond the named parameters get packed into a new tuple object at call time. Similarly, **kwargs triggers the construction of a new dictionary. This packing has a small, generally negligible cost — for the overwhelming majority of use cases, the flexibility is well worth it.
I can inspect this via the inspect module, which is genuinely useful for building generic tooling around function signatures:
import inspect
def example(a, b, *args, c=1, **kwargs):
pass
sig = inspect.signature(example)
for name, param in sig.parameters.items():
print(name, param.kind)
Output:
a POSITIONAL_OR_KEYWORD
b POSITIONAL_OR_KEYWORD
args VAR_POSITIONAL
c KEYWORD_ONLY
kwargs VAR_KEYWORD
Common Mistakes I’ve Made
- Assuming
**kwargspreserves insertion order in older Python versions. Since Python 3.7, dictionaries (and thereforekwargs) preserve insertion order as a language guarantee, but I still see people write defensive code for this that isn’t necessary anymore on modern Python. - Putting
*argsafter keyword arguments with defaults incorrectly and hitting aSyntaxError. The ordering rules aren’t optional — Python enforces them at parse time. - Overusing
**kwargsto the point where a function’s actual accepted parameters become undiscoverable without reading the entire function body. If a function always expects specific named parameters, I write them explicitly rather than hiding them inside**kwargs. - Forgetting that
*argsand**kwargsare local copies — reassigningkwargs = {}inside the function doesn’t affect the caller at all, following the same reference/reassignment rules covered in argument mutability.
FAQs
Do I have to name them args and kwargs? No — those are just conventions. *values and **options work exactly the same way; only the * and ** symbols matter to Python, not the names.
Can I have more than one *args or **kwargs in a single function definition? No, a function can only have one *args-style parameter and one **kwargs-style parameter.
What’s the difference between *args in a function definition vs. a function call? In a definition, it collects extra positional arguments into a tuple. In a call, it unpacks an iterable into separate positional arguments. The same duality applies to **kwargs with dictionaries.
Can *args and **kwargs be empty? Yes — if no extra positional or keyword arguments are passed, args is simply an empty tuple () and kwargs is an empty dictionary {}.
Summary
*args and **kwargs are what make Python functions genuinely flexible — letting me write decorators, wrapper functions, and extensible APIs without hardcoding a fixed argument list. Once I understood that they simply pack extra positional arguments into a tuple and extra keyword arguments into a dictionary (and unpack the reverse way when calling), the pattern became second nature. My rule of thumb: use explicit named parameters whenever the function’s interface is well-defined, and reach for *args/**kwargs when I genuinely need to forward or accept an unknown, variable set of arguments.