Python - Function Arguments (args, kwargs)

• Python supports four types of function arguments: positional, keyword, variable positional (*args), and variable keyword (**kwargs), each serving distinct use cases in API design and code...

Key Insights

• Python supports four types of function arguments: positional, keyword, variable positional (*args), and variable keyword (**kwargs), each serving distinct use cases in API design and code flexibility. • *args collects extra positional arguments into a tuple, while **kwargs collects extra keyword arguments into a dictionary, enabling functions to accept arbitrary numbers of parameters. • Argument order matters: positional arguments must come before *args, which must come before keyword arguments, which must come before **kwargs.

Positional and Keyword Arguments

Python functions accept arguments in two fundamental ways: positional and keyword. Positional arguments rely on order, while keyword arguments use explicit names.

def create_user(username, email, age):
    return {
        'username': username,
        'email': email,
        'age': age
    }

# Positional arguments
user1 = create_user('john_doe', 'john@example.com', 30)

# Keyword arguments
user2 = create_user(email='jane@example.com', username='jane_doe', age=25)

# Mixed (positional must come first)
user3 = create_user('bob', email='bob@example.com', age=35)

Default values make arguments optional:

def create_user(username, email, age=18, active=True):
    return {
        'username': username,
        'email': email,
        'age': age,
        'active': active
    }

user = create_user('alice', 'alice@example.com')
# {'username': 'alice', 'email': 'alice@example.com', 'age': 18, 'active': True}

Variable Positional Arguments (*args)

The *args syntax collects any number of positional arguments into a tuple. This enables functions to accept flexible argument counts without defining each parameter explicitly.

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

print(calculate_sum(1, 2, 3))  # 6
print(calculate_sum(10, 20, 30, 40, 50))  # 150

*args works alongside regular parameters:

def log_message(level, *messages):
    timestamp = "2024-01-01 12:00:00"
    combined = " ".join(str(msg) for msg in messages)
    print(f"[{timestamp}] {level}: {combined}")

log_message("ERROR", "Database", "connection", "failed")
# [2024-01-01 12:00:00] ERROR: Database connection failed

Real-world example with data processing:

def merge_datasets(*datasets):
    merged = []
    for dataset in datasets:
        merged.extend(dataset)
    return merged

data1 = [1, 2, 3]
data2 = [4, 5, 6]
data3 = [7, 8, 9]

result = merge_datasets(data1, data2, data3)
print(result)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

Variable Keyword Arguments (**kwargs)

The **kwargs syntax collects keyword arguments into a dictionary. The parameter name becomes the key, and the argument value becomes the dictionary value.

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

settings = create_config(
    host='localhost',
    port=5432,
    database='mydb',
    timeout=30
)
print(settings)
# {'host': 'localhost', 'port': 5432, 'database': 'mydb', 'timeout': 30}

**kwargs enables flexible API design:

def build_query(table, **conditions):
    query = f"SELECT * FROM {table}"
    if conditions:
        where_clauses = [f"{key} = '{value}'" for key, value in conditions.items()]
        query += " WHERE " + " AND ".join(where_clauses)
    return query

query1 = build_query('users', status='active', role='admin')
# SELECT * FROM users WHERE status = 'active' AND role = 'admin'

query2 = build_query('products', category='electronics', price=999)
# SELECT * FROM products WHERE category = 'electronics' AND price = '999'

Combining All Argument Types

Python enforces a specific order when combining argument types: positional, *args, keyword, **kwargs.

def complex_function(pos1, pos2, *args, key1='default', key2='default', **kwargs):
    print(f"Positional: {pos1}, {pos2}")
    print(f"Args: {args}")
    print(f"Keywords: {key1}, {key2}")
    print(f"Kwargs: {kwargs}")

complex_function(
    1, 2,           # pos1, pos2
    3, 4, 5,        # args
    key1='custom',  # keyword argument
    extra1='a',     # kwargs
    extra2='b'      # kwargs
)
# Positional: 1, 2
# Args: (3, 4, 5)
# Keywords: custom, default
# Kwargs: {'extra1': 'a', 'extra2': 'b'}

Practical example with HTTP request wrapper:

def make_request(method, url, *middleware, timeout=30, retry=3, **headers):
    request = {
        'method': method,
        'url': url,
        'timeout': timeout,
        'retry': retry,
        'middleware': middleware,
        'headers': headers
    }
    return request

request = make_request(
    'POST',
    'https://api.example.com/users',
    'auth_middleware',
    'logging_middleware',
    timeout=60,
    Authorization='Bearer token123',
    Content_Type='application/json'
)

Unpacking Arguments

The * and ** operators also unpack sequences and dictionaries when calling functions.

def create_point(x, y, z):
    return {'x': x, 'y': y, 'z': z}

coordinates = [10, 20, 30]
point = create_point(*coordinates)
print(point)  # {'x': 10, 'y': 20, 'z': 30}

params = {'x': 5, 'y': 15, 'z': 25}
point2 = create_point(**params)
print(point2)  # {'x': 5, 'y': 15, 'z': 25}

Combining unpacking with additional arguments:

def configure_server(host, port, *features, **settings):
    return {
        'host': host,
        'port': port,
        'features': features,
        'settings': settings
    }

base_config = ['ssl', 'compression']
extra_settings = {'max_connections': 100, 'timeout': 30}

server = configure_server(
    'localhost',
    8080,
    *base_config,
    'caching',
    **extra_settings,
    debug=True
)
# {
#     'host': 'localhost',
#     'port': 8080,
#     'features': ('ssl', 'compression', 'caching'),
#     'settings': {'max_connections': 100, 'timeout': 30, 'debug': True}
# }

Enforcing Argument Types

Python 3 introduced positional-only (/) and keyword-only (*) parameters for stricter API contracts.

def create_user(username, /, email, *, role='user', active=True):
    return {
        'username': username,
        'email': email,
        'role': role,
        'active': active
    }

# Valid calls
user1 = create_user('john', 'john@example.com')
user2 = create_user('jane', email='jane@example.com', role='admin')

# Invalid: username must be positional
# user3 = create_user(username='bob', email='bob@example.com')  # TypeError

# Invalid: role must be keyword
# user4 = create_user('alice', 'alice@example.com', 'admin')  # TypeError

This pattern prevents API misuse and improves code clarity:

def process_payment(amount, /, payment_method, *, currency='USD', fee=0):
    total = amount + fee
    return {
        'amount': amount,
        'method': payment_method,
        'currency': currency,
        'total': total
    }

# Clear, unambiguous calls
payment = process_payment(100.00, 'credit_card', currency='EUR', fee=2.50)

Common Patterns and Best Practices

Decorator pattern with *args and **kwargs:

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

@timer_decorator
def process_data(data, iterations=1000):
    for _ in range(iterations):
        _ = [x * 2 for x in data]
    return "Done"

process_data([1, 2, 3, 4, 5], iterations=5000)

Builder pattern for complex object creation:

class DatabaseConnection:
    def __init__(self, host, port, **options):
        self.host = host
        self.port = port
        self.pool_size = options.get('pool_size', 10)
        self.timeout = options.get('timeout', 30)
        self.ssl = options.get('ssl', False)
        self.retry_attempts = options.get('retry_attempts', 3)
    
    def __repr__(self):
        return f"DB({self.host}:{self.port}, pool={self.pool_size})"

db = DatabaseConnection(
    'localhost',
    5432,
    pool_size=20,
    ssl=True,
    timeout=60
)

Use *args and **kwargs when building flexible, extensible APIs. Use positional-only and keyword-only parameters when you need strict contracts. Always document expected argument types and behaviors for maintainable code.

Liked this? There's more.

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