Python *args and **kwargs Explained: The Complete Guide
Updated on
Every Python developer hits this wall at some point. You open a library's source code, a colleague's pull request, or an open-source project, and you see function signatures littered with *args and **kwargs. What do the asterisks mean? Why are there one and two of them? When should you use which? Getting them wrong produces confusing TypeError messages like "takes 2 positional arguments but 5 were given" or "got an unexpected keyword argument," and suddenly a straightforward function call turns into a debugging session.
The problem gets worse when you need to write flexible functions yourself. Hard-coding every parameter makes your API rigid. Accepting too many named parameters makes the function signature unreadable. You need a way to write functions that accept a variable number of arguments without sacrificing clarity.
Python solves this with two special syntax features: *args for variable positional arguments and **kwargs for variable keyword arguments. This guide explains both from the ground up, covers the unpacking operators, walks through real-world patterns, and helps you avoid the most common mistakes.
What Are *args and **kwargs?
In Python, *args and **kwargs are conventions for accepting a variable number of arguments in function definitions.
*argscollects extra positional arguments into a tuple.**kwargscollects extra keyword arguments into a dictionary.
The names args and kwargs are conventions, not requirements. The magic comes from the * and ** prefixes, not the names themselves. You could write *values and **options and they would work identically.
Here is the simplest demonstration:
def show_args(*args, **kwargs):
print(f"args = {args}")
print(f"kwargs = {kwargs}")
show_args(1, 2, 3, name="Alice", age=30)
# args = (1, 2, 3)
# kwargs = {'name': 'Alice', 'age': 30}Positional arguments (values passed without names) land in args as a tuple. Keyword arguments (values passed with key=value syntax) land in kwargs as a dictionary. That is the entire core concept.
Understanding *args: Variable Positional Arguments
The single asterisk * before a parameter name tells Python to pack all remaining positional arguments into a tuple. This lets your function accept any number of positional values.
Basic Syntax and Usage
def add_all(*args):
"""Sum any number of values."""
total = 0
for num in args:
total += num
return total
print(add_all(1, 2)) # 3
print(add_all(1, 2, 3, 4, 5)) # 15
print(add_all(10)) # 10
print(add_all()) # 0Inside the function, args is a regular Python tuple. You can iterate over it, index into it, check its length, and pass it to other functions.
def describe_args(*args):
print(f"Type: {type(args)}")
print(f"Length: {len(args)}")
print(f"First element: {args[0] if args else 'N/A'}")
print(f"Contents: {args}")
describe_args("hello", 42, True)
# Type: <class 'tuple'>
# Length: 3
# First element: hello
# Contents: ('hello', 42, True)Combining Regular Parameters with *args
You can mix standard parameters with *args. All regular positional parameters are filled first, and any remaining positional arguments go into *args:
def log_message(level, *args):
"""Log a message with a severity level."""
message = " ".join(str(a) for a in args)
print(f"[{level.upper()}] {message}")
log_message("info", "Server started on port", 8080)
# [INFO] Server started on port 8080
log_message("error", "Connection failed:", "timeout after", 30, "seconds")
# [ERROR] Connection failed: timeout after 30 secondsReal-World Example: A Flexible Average Function
def average(*values):
"""Calculate the average of any number of values."""
if not values:
raise ValueError("average() requires at least one argument")
return sum(values) / len(values)
print(average(85, 90, 78)) # 84.33333333333333
print(average(100)) # 100.0
print(average(72, 88, 95, 67, 91)) # 82.6Real-World Example: String Formatting Helper
def build_path(*segments):
"""Join path segments with forward slashes, stripping extras."""
cleaned = [seg.strip("/") for seg in segments if seg]
return "/" + "/".join(cleaned)
print(build_path("api", "v2", "users", "123"))
# /api/v2/users/123
print(build_path("/data/", "/reports/", "2026/", "sales.csv"))
# /data/reports/2026/sales.csvUnderstanding **kwargs: Variable Keyword Arguments
The double asterisk ** before a parameter name tells Python to pack all remaining keyword arguments into a dictionary. This lets your function accept any number of named values.
Basic Syntax and Usage
def print_info(**kwargs):
"""Print key-value pairs in a formatted way."""
for key, value in kwargs.items():
print(f" {key}: {value}")
print_info(name="Alice", age=30, city="Seattle")
# name: Alice
# age: 30
# city: SeattleInside the function, kwargs is a standard Python dictionary. You can use .get(), .keys(), .values(), .items(), and any other dict method.
def describe_kwargs(**kwargs):
print(f"Type: {type(kwargs)}")
print(f"Keys: {list(kwargs.keys())}")
print(f"Values: {list(kwargs.values())}")
describe_kwargs(x=10, y=20, z=30)
# Type: <class 'dict'>
# Keys: ['x', 'y', 'z']
# Values: [10, 20, 30]Real-World Example: Configuration Builder
def create_connection(host, port, **kwargs):
"""Create a database connection with optional settings."""
config = {
"host": host,
"port": port,
"timeout": kwargs.get("timeout", 30),
"retries": kwargs.get("retries", 3),
"ssl": kwargs.get("ssl", True),
"pool_size": kwargs.get("pool_size", 5),
}
# Add any extra settings the caller provided
for key, value in kwargs.items():
if key not in config:
config[key] = value
return config
# Basic usage
basic = create_connection("localhost", 5432)
print(basic)
# {'host': 'localhost', 'port': 5432, 'timeout': 30, 'retries': 3, 'ssl': True, 'pool_size': 5}
# With custom options
custom = create_connection(
"db.example.com", 5432,
timeout=60,
ssl=False,
application_name="my_app"
)
print(custom)
# {'host': 'db.example.com', 'port': 5432, 'timeout': 60, 'retries': 3, 'ssl': False, 'pool_size': 5, 'application_name': 'my_app'}Real-World Example: HTML Tag Builder
def html_tag(tag, content="", **attributes):
"""Generate an HTML tag with optional attributes."""
attr_str = ""
for key, value in attributes.items():
# Convert Python naming to HTML (class_ -> class)
html_key = key.rstrip("_")
attr_str += f' {html_key}="{value}"'
if content:
return f"<{tag}{attr_str}>{content}</{tag}>"
return f"<{tag}{attr_str} />"
print(html_tag("a", "Click here", href="https://example.com", class_="btn"))
# <a href="https://example.com" class="btn">Click here</a>
print(html_tag("img", src="photo.jpg", alt="A photo", width="200"))
# <img src="photo.jpg" alt="A photo" width="200" />
print(html_tag("p", "Hello world", id="intro", style="color: blue"))
# <p id="intro" style="color: blue">Hello world</p>Using *args and **kwargs Together
You can use both in the same function definition to accept any combination of positional and keyword arguments. The order of parameters follows strict rules.
Parameter Ordering Rules
Python enforces this exact order in function signatures:
- Regular positional parameters
*args(variable positional)- Keyword-only parameters (after
*args) **kwargs(variable keyword)
def example(a, b, *args, option=True, **kwargs):
print(f"a = {a}")
print(f"b = {b}")
print(f"args = {args}")
print(f"option = {option}")
print(f"kwargs = {kwargs}")
example(1, 2, 3, 4, 5, option=False, color="red", size=10)
# a = 1
# b = 2
# args = (3, 4, 5)
# option = False
# kwargs = {'color': 'red', 'size': 10}Here is a summary of the parameter types and their order:
| Position | Type | Syntax | Example | Description |
|---|---|---|---|---|
| 1st | Positional | param | a, b | Required, filled by position |
| 2nd | Default | param=value | c=10 | Optional, filled by position or name |
| 3rd | Variable positional | *args | *args | Collects extra positional args |
| 4th | Keyword-only | param (after *) | option=True | Must be passed by name |
| 5th | Variable keyword | **kwargs | **kwargs | Collects extra keyword args |
Common Pattern: Pass-Through Function
One of the most useful patterns with *args and **kwargs is creating functions that pass all arguments through to another function:
def timed_call(func, *args, **kwargs):
"""Call a function and measure its execution time."""
import time
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
def expensive_sum(a, b, c):
import time
time.sleep(0.1)
return a + b + c
result = timed_call(expensive_sum, 10, 20, c=30)
# expensive_sum took 0.1003s
print(result)
# 60Unpacking with * and **
The * and ** operators work in both directions: they pack arguments in function definitions, and they unpack arguments in function calls and other contexts.
Unpacking Lists and Tuples with *
Use * to unpack an iterable into individual positional arguments:
def add(a, b, c):
return a + b + c
numbers = [10, 20, 30]
# Without unpacking - this causes TypeError
# add(numbers) # TypeError: add() missing 2 required positional arguments
# With unpacking - spreads list into separate arguments
result = add(*numbers)
print(result) # 60
# Works with tuples, sets, and any iterable
coords = (5, 10, 15)
print(add(*coords)) # 30You can also use * for unpacking in assignments and list construction (Python 3.5+):
# Extended unpacking in assignments
first, *middle, last = [1, 2, 3, 4, 5]
print(first) # 1
print(middle) # [2, 3, 4]
print(last) # 5
# Unpacking in list/tuple construction
list_a = [1, 2, 3]
list_b = [4, 5, 6]
combined = [*list_a, *list_b]
print(combined) # [1, 2, 3, 4, 5, 6]
# Unpacking with additional elements
extended = [0, *list_a, 99, *list_b, 100]
print(extended) # [0, 1, 2, 3, 99, 4, 5, 6, 100]Unpacking Dictionaries with **
Use ** to unpack a dictionary into keyword arguments:
def create_user(name, email, role="viewer"):
return {"name": name, "email": email, "role": role}
user_data = {"name": "Alice", "email": "alice@example.com", "role": "admin"}
# Unpack dict into keyword arguments
user = create_user(**user_data)
print(user)
# {'name': 'Alice', 'email': 'alice@example.com', 'role': 'admin'}Merging Dictionaries with **
One of the most practical uses of ** is merging dictionaries:
defaults = {"color": "blue", "size": 12, "font": "Arial"}
user_prefs = {"color": "red", "size": 16}
# Merge: user_prefs override defaults
merged = {**defaults, **user_prefs}
print(merged)
# {'color': 'red', 'size': 16, 'font': 'Arial'}
# Python 3.9+ also supports the | operator
merged_new = defaults | user_prefs
print(merged_new)
# {'color': 'red', 'size': 16, 'font': 'Arial'}
# Adding extra keys during merge
final = {**defaults, **user_prefs, "theme": "dark"}
print(final)
# {'color': 'red', 'size': 16, 'font': 'Arial', 'theme': 'dark'}Combining * and ** Unpacking
You can use both operators together when calling a function:
def report(title, *items, separator="---", **metadata):
print(f"== {title} ==")
for item in items:
print(f" - {item}")
print(separator)
for key, value in metadata.items():
print(f" {key}: {value}")
positional = ["Task A", "Task B", "Task C"]
options = {"author": "Alice", "date": "2026-02-14"}
report("Sprint Review", *positional, separator="===", **options)
# == Sprint Review ==
# - Task A
# - Task B
# - Task C
# ===
# author: Alice
# date: 2026-02-14Practical Patterns
Pattern 1: Decorator Functions
The most common use of *args and **kwargs in production Python is writing decorators. A decorator wraps one function with another. Since you do not know the wrapped function's signature in advance, you must use *args and **kwargs to forward all arguments:
import functools
import time
def retry(max_attempts=3, delay=1.0):
"""Retry a function up to max_attempts times on failure."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
print(f"Attempt {attempt}/{max_attempts} failed: {e}")
if attempt < max_attempts:
time.sleep(delay)
raise last_exception
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def fetch_data(url, timeout=10):
"""Simulate fetching data that might fail."""
import random
if random.random() < 0.6:
raise ConnectionError(f"Failed to connect to {url}")
return f"Data from {url}"
# The decorator forwards url and timeout through *args/**kwargs
result = fetch_data("https://api.example.com", timeout=5)
print(result)Pattern 2: Subclass init Forwarding
When subclassing, you often need to forward constructor arguments to the parent class. *args and **kwargs make this clean:
class Animal:
def __init__(self, name, species, sound="..."):
self.name = name
self.species = species
self.sound = sound
def speak(self):
return f"{self.name} says {self.sound}"
class Dog(Animal):
def __init__(self, *args, breed="Unknown", **kwargs):
super().__init__(*args, **kwargs)
self.breed = breed
def info(self):
return f"{self.name} ({self.breed}) - {self.species}"
# All Animal parameters pass through seamlessly
dog = Dog("Rex", "Canine", sound="Woof!", breed="German Shepherd")
print(dog.speak()) # Rex says Woof!
print(dog.info()) # Rex (German Shepherd) - CanineThis pattern is essential when working with complex class hierarchies, especially in frameworks like Django, Flask, or SQLAlchemy where you extend base classes.
Pattern 3: Wrapper and Proxy Functions
When you need to intercept or modify function calls without changing the original function:
def log_call(func):
"""Log every call to a function with its arguments."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
print(f"Calling {func.__name__}({signature})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper
@log_call
def calculate_discount(price, discount_pct, tax_rate=0.08):
discounted = price * (1 - discount_pct / 100)
return round(discounted * (1 + tax_rate), 2)
calculate_discount(100, 20, tax_rate=0.1)
# Calling calculate_discount(100, 20, tax_rate=0.1)
# calculate_discount returned 88.0Pattern 4: API Client Builders
Building flexible API wrappers is a classic use case:
import json
class APIClient:
def __init__(self, base_url, **default_headers):
self.base_url = base_url.rstrip("/")
self.default_headers = {
"Content-Type": "application/json",
"Accept": "application/json",
**default_headers,
}
def request(self, method, endpoint, *args, **kwargs):
"""Build a request with merged headers and parameters."""
url = f"{self.base_url}/{endpoint.lstrip('/')}"
headers = {**self.default_headers, **kwargs.pop("headers", {})}
request_info = {
"method": method,
"url": url,
"headers": headers,
**kwargs,
}
print(json.dumps(request_info, indent=2, default=str))
return request_info
def get(self, endpoint, **kwargs):
return self.request("GET", endpoint, **kwargs)
def post(self, endpoint, **kwargs):
return self.request("POST", endpoint, **kwargs)
# Usage
client = APIClient(
"https://api.example.com",
Authorization="Bearer token123"
)
client.get("/users", params={"page": 1, "limit": 50})
client.post("/users", data={"name": "Alice"}, headers={"X-Request-ID": "abc123"})Pattern 5: Data Science -- Dynamic Plotting Parameters
When building data analysis functions, **kwargs lets you pass configuration through to underlying libraries:
import pandas as pd
def analyze_column(df, column, **plot_kwargs):
"""Analyze a dataframe column and generate summary statistics."""
stats = {
"count": df[column].count(),
"mean": df[column].mean(),
"std": df[column].std(),
"min": df[column].min(),
"max": df[column].max(),
}
print(f"\nAnalysis of '{column}':")
for stat, value in stats.items():
print(f" {stat}: {value:.2f}")
# Forward any extra kwargs to the plot function
plot_defaults = {"kind": "hist", "bins": 20, "title": f"Distribution of {column}"}
plot_config = {**plot_defaults, **plot_kwargs}
# df[column].plot(**plot_config) # Uncomment with matplotlib installed
print(f" Plot config: {plot_config}")
return stats
# Create sample data
df = pd.DataFrame({
"revenue": [100, 250, 180, 320, 275, 410, 195, 360],
"quantity": [5, 12, 8, 15, 13, 20, 9, 17],
})
# Default analysis
analyze_column(df, "revenue")
# Custom plot settings passed through **kwargs
analyze_column(df, "revenue", kind="box", color="steelblue", figsize=(10, 6))If you work in Jupyter notebooks and want to experiment with function signatures interactively, RunCell (opens in a new tab) provides an AI-powered notebook environment where you can test *args and **kwargs patterns, get real-time suggestions for parameter handling, and debug argument-passing issues without leaving your workflow.
Common Mistakes and How to Fix Them
Here are the most frequent errors Python developers encounter with *args and **kwargs, along with their solutions:
| Mistake | Error Message | Cause | Fix |
|---|---|---|---|
| Wrong parameter order | SyntaxError: invalid syntax | Placing **kwargs before *args | Always use order: regular, *args, keyword-only, **kwargs |
| Passing a list instead of unpacking | TypeError: func() missing required arguments | Passing [1,2,3] instead of *[1,2,3] | Use * to unpack: func(*my_list) |
| Duplicate keyword argument | TypeError: got multiple values for argument 'x' | Same key in both positional and **kwargs | Ensure no overlap between positional args and dict keys |
| Modifying kwargs directly | Unexpected side effects | Mutating the kwargs dict | Use kwargs.copy() or {**kwargs, ...} |
Forgetting *args in super().init | TypeError: __init__() missing arguments | Not forwarding args to parent class | Use super().__init__(*args, **kwargs) |
| Using mutable default with kwargs | Shared state between calls | def func(data={}) | Use None as default: def func(data=None) |
Example: Duplicate Keyword Argument
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
data = {"name": "Alice", "greeting": "Hi"}
# WRONG: name is passed both as positional AND in **data
# greet("Alice", **data)
# TypeError: greet() got multiple values for argument 'name'
# CORRECT: pass only through unpacking
print(greet(**data))
# Hi, Alice!
# OR: remove the duplicate key
print(greet("Alice", **{"greeting": "Hi"}))
# Hi, Alice!Example: Modifying kwargs Safely
def process(name, **kwargs):
# WRONG: modifying kwargs directly affects the caller's dict
# kwargs["processed"] = True
# CORRECT: create a new dict
config = {**kwargs, "processed": True}
return {"name": name, **config}
settings = {"timeout": 30, "retries": 3}
result = process("task1", **settings)
print(result)
# {'name': 'task1', 'timeout': 30, 'retries': 3, 'processed': True}
# Original dict is unchanged
print(settings)
# {'timeout': 30, 'retries': 3}*args/**kwargs vs Other Approaches
When should you use *args and **kwargs instead of alternatives? Here is a comparison:
| Approach | Syntax | Best For | Drawbacks |
|---|---|---|---|
*args | def f(*args) | Unknown number of same-type positional values | No type hints per argument, no named access |
**kwargs | def f(**kwargs) | Flexible options, pass-through to other functions | No auto-complete in IDEs, no static type checking |
| Explicit parameters | def f(a, b, c) | Known, fixed set of arguments | Rigid; adding parameters breaks existing calls |
| Default parameters | def f(a, b=10) | Optional parameters with sensible defaults | Still requires knowing all options upfront |
| List/tuple parameter | def f(items: list) | Ordered collection of values | Caller must construct the list explicitly |
| Dict parameter | def f(options: dict) | Structured configuration | Caller must construct the dict explicitly |
| TypedDict/dataclass | def f(config: Config) | Structured, type-safe configuration | More boilerplate, requires class definition |
General guidelines:
- Use explicit parameters when the set of arguments is known and stable.
- Use
*argswhen your function naturally operates on a variable number of same-type values (likeprint(),max(),min()). - Use
**kwargswhen you need to forward options to another function, accept flexible configuration, or build extensible APIs. - Use both when writing decorators, proxy functions, or class hierarchies that need full argument forwarding.
- Use TypedDict or dataclass when you want IDE auto-complete and static type checking for structured configuration.
Adding Type Hints with *args and **kwargs
Python 3.11+ allows you to add type hints using *args and **kwargs with Unpack and TypedDict:
# Basic type hints (all args same type)
def add_numbers(*args: float) -> float:
return sum(args)
def set_options(**kwargs: str) -> dict[str, str]:
return kwargs
# Python 3.11+: precise kwargs typing with TypedDict
from typing import TypedDict, Unpack
class ConnectionOptions(TypedDict, total=False):
timeout: int
retries: int
ssl: bool
def connect(host: str, port: int, **kwargs: Unpack[ConnectionOptions]) -> dict:
return {"host": host, "port": port, **kwargs}
# IDE now knows the valid keyword arguments
result = connect("localhost", 5432, timeout=30, ssl=True)
print(result)
# {'host': 'localhost', 'port': 5432, 'timeout': 30, 'ssl': True}Frequently Asked Questions
What do *args and **kwargs stand for?
The names args and kwargs are short for "arguments" and "keyword arguments" respectively. They are purely conventional names -- the actual magic comes from the * and ** prefix operators. The single asterisk * tells Python to pack extra positional arguments into a tuple, while the double asterisk ** packs extra keyword arguments into a dictionary. You will see these names used in virtually every Python codebase, tutorial, and library because they are the universally recognized convention, but the language does not require these specific names.
Can I use names other than args and kwargs?
Yes. The names args and kwargs are conventions, not syntax requirements. The unpacking behavior comes entirely from the * and ** prefixes. You can write *values, *items, *numbers, **options, **config, **params, or any other valid Python identifier. However, sticking with *args and **kwargs is strongly recommended in most cases because every Python developer immediately recognizes them. Use custom names only when a more descriptive name genuinely improves readability, such as *paths in a file-handling function or **headers in an HTTP client.
What is the correct order of parameters in a Python function?
Python enforces a strict ordering: regular positional parameters come first, then *args, then keyword-only parameters (with default values), and finally **kwargs. The full order is: def func(pos1, pos2, default1=val, *args, kw_only1, kw_only2=val, **kwargs). Violating this order produces a SyntaxError. A useful mnemonic is "positional, star-args, keyword-only, double-star-kwargs" -- the number of stars increases from left to right.
When should I use *args vs a list parameter?
Use *args when each argument is a separate, independent value and the caller should pass them naturally without constructing a container: print("a", "b", "c") is more natural than print(["a", "b", "c"]). Use a list parameter when the values logically form a collection that the caller already has in a variable, or when you need to distinguish between the collection and other parameters. Built-in functions like max(), min(), and print() use *args because the calling convention feels natural, while functions like sorted(iterable) take a single iterable because the input is inherently a sequence.
Are *args and **kwargs slow?
The overhead of *args and **kwargs is minimal. Python creates a tuple for *args and a dictionary for **kwargs on each call, which involves small memory allocations. In benchmarks, the difference compared to explicit parameters is typically a few hundred nanoseconds per call -- irrelevant for virtually all real-world code. You would need millions of calls in a tight loop before this overhead becomes measurable. Focus on algorithmic efficiency and I/O optimization rather than avoiding *args/**kwargs. The flexibility and code maintainability they provide far outweigh any micro-performance cost.
Conclusion
Python's *args and **kwargs are two of the most practical features in the language. They solve a fundamental problem: how to write functions that are flexible enough to accept varying numbers of arguments without sacrificing readability.
The key takeaways:
*argscollects extra positional arguments into a tuple. Use it when your function should accept any number of values.**kwargscollects extra keyword arguments into a dictionary. Use it for flexible options, configuration pass-through, and extensible APIs.- Parameter order is always: regular,
*args, keyword-only,**kwargs. - Unpacking with
*and**works in function calls, list construction, and dictionary merging. - Decorators are the most important real-world use case -- they rely on
*argsand**kwargsto wrap any function regardless of its signature.
Start with explicit parameters for functions with known, stable signatures. Reach for *args and **kwargs when you need flexibility: decorators, subclass forwarding, wrapper functions, and API builders. Once you internalize the packing and unpacking mechanics, you will find yourself writing cleaner, more reusable Python code.