Technology  /  Python

🐍 Python 78 guides · updated 2026

From first variable to OOP, generators, and real projects β€” the language that runs everything from data pipelines to AI agents, taught the practical way.

Python *args and **kwargs: Variable-Length Arguments Without the Confusion

Most function definitions list a fixed number of parameters. But sometimes you do not know in advance how many arguments a caller will provide. Python handles this with *args for variable-length positional arguments and **kwargs for variable-length keyword arguments.

*args: Collecting Extra Positional Arguments

When you prefix a parameter with *, Python collects all positional arguments beyond the explicitly named ones into a tuple bound to that parameter name.

def total(*numbers):
return sum(numbers)
print(total(1, 2, 3)) # 6
print(total(10, 20, 30, 40)) # 100
print(total()) # 0 β€” empty tuple, sum returns 0

Inside the function, numbers is a regular tuple. You can iterate over it, pass it to sum(), slice it, or check its length.

The name args is conventional, not required. *values, *items, or any other name works just as well. The * is what makes it collect.

def first_and_rest(first, *rest):
print(f"First: {first}")
print(f"Rest: {rest}")
first_and_rest("a", "b", "c", "d")
# First: a
# Rest: ('b', 'c', 'd')

The first positional argument goes to first as normal. Everything else goes into rest.

**kwargs: Collecting Extra Keyword Arguments

** collects all keyword arguments that were not matched by named parameters into a dict.

def show_info(**details):
for key, value in details.items():
print(f"{key}: {value}")
show_info(name="Alice", age=30, city="London")
# name: Alice
# age: 30
# city: London

The caller passes name="Alice" and age=30 β€” both are keyword arguments. Inside the function, details is {"name": "Alice", "age": 30, "city": "London"}.

Using Both Together

You can combine *args and **kwargs in the same function. The order matters: positional parameters first, then *args, then named keyword parameters (with or without defaults), then **kwargs.

def create_tag(tag_name, *children, class_name=None, **attributes):
attrs = ""
if class_name:
attrs += f' class="{class_name}"'
for key, value in attributes.items():
attrs += f' {key}="{value}"'
inner = "".join(str(c) for c in children)
return f"<{tag_name}{attrs}>{inner}</{tag_name}>"
print(create_tag("p", "Hello, world!"))
# <p>Hello, world!</p>
print(create_tag("a", "Click here", class_name="link", href="/about", target="_blank"))
# <a class="link" href="/about" target="_blank">Click here</a>

tag_name is a required positional argument. *children collects any additional positional arguments (the text or nested tags). class_name is a keyword-only parameter with a default. **attributes collects any other keyword arguments as HTML attributes.

Argument Order Rules

Python enforces a strict ordering for function parameters:

  1. Regular positional parameters (a, b, c)
  2. *args (or bare * for keyword-only without collecting)
  3. Keyword-only parameters (after *)
  4. **kwargs
# Valid
def func(a, b, *args, keyword_only, **kwargs):
pass
# Invalid β€” **kwargs must come last
def bad(a, **kwargs, b): # SyntaxError
pass

Unpacking When Calling

The same * and ** syntax also works at the call site to unpack sequences into positional arguments and dictionaries into keyword arguments.

def add(a, b, c):
return a + b + c
values = [1, 2, 3]
print(add(*values)) # 6 β€” equivalent to add(1, 2, 3)
settings = {"a": 10, "b": 20, "c": 30}
print(add(**settings)) # 60 β€” equivalent to add(a=10, b=20, c=30)

This is particularly useful when you have data in a list or dict and want to pass it to a function that expects individual arguments.

Real Use Case: Decorators and Wrappers

Decorators need to forward all arguments to the wrapped function without knowing what those arguments are. *args and **kwargs make this possible.

import time
import functools
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs) # forward everything
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timed
def slow_operation(n, multiplier=1):
return sum(range(n)) * multiplier
print(slow_operation(1_000_000))
print(slow_operation(500_000, multiplier=2))

wrapper does not need to know the signature of func. It collects all arguments and forwards them intact.

Real Use Case: Extending a Parent Class Constructor

class LoggedDict(dict):
def __init__(self, *args, log_level="INFO", **kwargs):
super().__init__(*args, **kwargs) # forward everything to dict
self.log_level = log_level
print(f"[{log_level}] LoggedDict created with {len(self)} items")
d = LoggedDict({"a": 1, "b": 2}, log_level="DEBUG")
# [DEBUG] LoggedDict created with 2 items
print(d["a"]) # 1

The *args and **kwargs let LoggedDict.__init__ accept everything that dict.__init__ accepts, while still intercepting log_level for its own use.

Common Mistakes

Wrong parameter order. Placing **kwargs before *args or before keyword-only parameters is a SyntaxError.

Assuming args is a list. It is a tuple. If you need to modify it, convert: args = list(args).

Confusing the definition with the call syntax. In the function signature, *args collects. At the call site, *my_list unpacks. Both use *, but they do opposite things.

Overusing **kwargs to avoid defining parameters. If a function needs specific keyword arguments, name them explicitly. Using **kwargs for everything hides the function’s interface and makes it harder to use and test.