Python *args and **kwargs: Variable Arguments Explained

Python functions typically require you to define each parameter explicitly. But what happens when you need a function that accepts any number of arguments? Consider a simple scenario:

Key Insights

  • *args collects variable positional arguments into a tuple, while **kwargs collects variable keyword arguments into a dictionary, enabling functions to accept flexible numbers of parameters
  • The order matters when combining parameter types: regular positional, *args, keyword-only, then **kwargs must follow this sequence
  • The * and ** operators work bidirectionally—they pack arguments when defining functions and unpack sequences/dictionaries when calling functions

Understanding the Problem

Python functions typically require you to define each parameter explicitly. But what happens when you need a function that accepts any number of arguments? Consider a simple scenario:

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

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

# What if we need to add four numbers? Five? Ten?

This approach doesn’t scale. You’d need to write a different function for every possible number of arguments. Variable arguments solve this problem by allowing functions to accept flexible numbers of parameters without defining each one individually.

Understanding *args: Positional Variable Arguments

The *args syntax collects all extra positional arguments passed to a function into a tuple. The asterisk (*) is the operator that does the work—args is just a conventional name. You could use *numbers or *values, but *args is the Python community standard.

Here’s a basic example:

def sum_all(*args):
    total = 0
    for num in args:
        total += num
    return total

print(sum_all(1, 2, 3))           # 6
print(sum_all(1, 2, 3, 4, 5))     # 15
print(sum_all(10))                # 10

Inside the function, args is a regular tuple. You can iterate over it, index it, or use any tuple method:

def analyze_args(*args):
    print(f"Received {len(args)} arguments")
    print(f"First argument: {args[0]}")
    print(f"All arguments: {args}")

analyze_args(10, 20, 30)
# Received 3 arguments
# First argument: 10
# All arguments: (10, 20, 30)

You can combine regular parameters with *args, but regular parameters must come first:

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

greet("Hello", "Alice", "Bob", "Charlie")
# Hello, Alice!
# Hello, Bob!
# Hello, Charlie!

Understanding **kwargs: Keyword Variable Arguments

While *args handles positional arguments, **kwargs (keyword arguments) collects extra keyword arguments into a dictionary. Again, kwargs is conventional—you could use **options or **params.

def build_profile(**kwargs):
    profile = {}
    for key, value in kwargs.items():
        profile[key] = value
    return profile

user = build_profile(name="Alice", age=30, city="New York", occupation="Engineer")
print(user)
# {'name': 'Alice', 'age': 30, 'city': 'New York', 'occupation': 'Engineer'}

The kwargs parameter is a standard Python dictionary with all the usual methods:

def print_config(**kwargs):
    print("Configuration:")
    for key, value in kwargs.items():
        print(f"  {key}: {value}")
    
    if 'debug' in kwargs:
        print("Debug mode is enabled")

print_config(host="localhost", port=8000, debug=True)
# Configuration:
#   host: localhost
#   port: 8000
#   debug: True
# Debug mode is enabled

You can mix regular parameters with **kwargs, but regular parameters must be defined first:

def create_user(username, email, **additional_info):
    user = {
        'username': username,
        'email': email,
        **additional_info  # Unpacking additional_info into user dict
    }
    return user

user = create_user("alice", "alice@example.com", age=30, city="NYC")
print(user)
# {'username': 'alice', 'email': 'alice@example.com', 'age': 30, 'city': 'NYC'}

Combining *args and **kwargs

You can use both *args and **kwargs in the same function signature. The order is crucial:

  1. Regular positional parameters
  2. *args
  3. Keyword-only parameters (optional)
  4. **kwargs
def complex_function(a, b, *args, option=None, **kwargs):
    print(f"a: {a}")
    print(f"b: {b}")
    print(f"args: {args}")
    print(f"option: {option}")
    print(f"kwargs: {kwargs}")

complex_function(1, 2, 3, 4, 5, option="test", x=10, y=20)
# a: 1
# b: 2
# args: (3, 4, 5)
# option: test
# kwargs: {'x': 10, 'y': 20}

Here’s a practical example—a flexible logging function:

def log_event(level, message, *tags, timestamp=None, **metadata):
    log_entry = {
        'level': level,
        'message': message,
        'tags': tags,
        'timestamp': timestamp or 'now',
        'metadata': metadata
    }
    print(f"[{log_entry['level'].upper()}] {log_entry['message']}")
    if tags:
        print(f"Tags: {', '.join(tags)}")
    for key, value in metadata.items():
        print(f"  {key}: {value}")

log_event("info", "User logged in", "auth", "security", 
          user_id=123, ip="192.168.1.1")
# [INFO] User logged in
# Tags: auth, security
#   user_id: 123
#   ip: 192.168.1.1

Unpacking Arguments with * and **

The * and ** operators work in reverse when calling functions. Use * to unpack sequences (lists, tuples) and ** to unpack dictionaries:

def calculate(a, b, c):
    return a + b * c

numbers = [2, 3, 4]
result = calculate(*numbers)  # Unpacks to calculate(2, 3, 4)
print(result)  # 14

# With dictionaries
params = {'a': 2, 'b': 3, 'c': 4}
result = calculate(**params)  # Unpacks to calculate(a=2, b=3, c=4)
print(result)  # 14

This is particularly useful for configuration dictionaries:

def connect_database(host, port, username, password, timeout=30):
    print(f"Connecting to {host}:{port} as {username}")
    return f"Connected with {timeout}s timeout"

db_config = {
    'host': 'localhost',
    'port': 5432,
    'username': 'admin',
    'password': 'secret',
    'timeout': 60
}

connection = connect_database(**db_config)

Common Pitfalls and Best Practices

Order matters. This will fail:

# WRONG - **kwargs must be last
def wrong_order(*args, **kwargs, option):
    pass

# CORRECT
def correct_order(*args, option, **kwargs):
    pass

Type hints improve code clarity (Python 3.5+):

from typing import Any

def process_data(*args: int, **kwargs: Any) -> dict:
    return {
        'sum': sum(args),
        'options': kwargs
    }

Avoid overusing variable arguments. They reduce code clarity. If your function consistently needs the same parameters, define them explicitly:

# Less clear
def create_user(**kwargs):
    return User(kwargs['username'], kwargs['email'])

# More clear
def create_user(username: str, email: str, **additional_attrs):
    return User(username, email, **additional_attrs)

Document expected arguments when using **kwargs:

def configure_app(**options):
    """
    Configure the application.
    
    Args:
        **options: Configuration options
            - debug (bool): Enable debug mode
            - port (int): Server port number
            - host (str): Server hostname
    """
    pass

Practical Applications

Variable arguments shine in decorators, where you need to wrap functions with unknown signatures:

import functools
import time

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

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

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

calculate_sum(1, 2, 3)  # Works with any function signature
greet("Alice")

They’re also essential for class inheritance when you want to pass arguments up to parent classes:

class BaseHandler:
    def __init__(self, name, **options):
        self.name = name
        self.options = options

class FileHandler(BaseHandler):
    def __init__(self, filename, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.filename = filename

handler = FileHandler("log.txt", "Logger", buffer_size=1024, encoding="utf-8")

Understanding *args and **kwargs is essential for writing flexible, reusable Python code. They enable powerful patterns like decorators and wrapper functions while keeping your code DRY. Use them judiciously—explicit is better than implicit, but when you need flexibility, these tools are indispensable.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.