Home Python *args and **kwargs in Python Explained — With Real-World Patterns

*args and **kwargs in Python Explained — With Real-World Patterns

In Plain English 🔥
Imagine you're a pizza chef. Sometimes a customer orders one topping, sometimes ten — you can't know in advance. So instead of asking 'how many toppings?' before you start, you just say 'tell me whatever toppings you want and I'll handle them.' args is your list of toppings (unnamed items in order). *kwargs is the customer saying 'extra cheese: double, crust: thin' — named preferences. Your function stays flexible regardless of what arrives at the counter.
⚡ Quick Answer
Imagine you're a pizza chef. Sometimes a customer orders one topping, sometimes ten — you can't know in advance. So instead of asking 'how many toppings?' before you start, you just say 'tell me whatever toppings you want and I'll handle them.' args is your list of toppings (unnamed items in order). *kwargs is the customer saying 'extra cheese: double, crust: thin' — named preferences. Your function stays flexible regardless of what arrives at the counter.

Most Python tutorials show you args and kwargs as a syntax curiosity — a footnote after the 'real' stuff. That's backwards. These two features are why Python's standard library, every major framework, and every well-designed API you've ever touched actually works. Django's class-based views use kwargs to pass URL parameters. Python's built-in print() uses args so you can pass it one string or twenty. Decorators — the feature that powers Flask routes, pytest fixtures, and logging middleware — are almost impossible to write correctly without them.

Why Fixed-Argument Functions Break in the Real World

When you define a function like def add(a, b), you're making a promise: this function always takes exactly two things. That's fine for a calculator. It's not fine for the real world, where requirements shift.

Picture a logging utility. On day one you log a message and a level. On day two someone needs to attach a user ID. Day three, a request ID. If your function signature is rigid, you're back editing it every time. You'd end up with def log(message, level, user_id=None, request_id=None, session_id=None) — a parameter list that grows forever.

args and kwargs exist to solve exactly this. They're not shortcuts or hacks — they're a deliberate design pattern that says 'this function is designed to receive a variable number of inputs.' Understanding when to reach for them (and when not* to) is what separates intermediate from advanced Python.

rigid_vs_flexible_logger.py · PYTHON
12345678910111213141516171819202122232425262728293031
# THE PROBLEM: a rigid function that can't grow cleanly
def log_rigid(message, level, user_id=None, request_id=None):
    # Adding new optional fields means changing the signature every time
    print(f'[{level}] {message} | user={user_id} | request={request_id}')


# THE SOLUTION: a flexible function using *args and **kwargs
def log_flexible(message, level, *extra_values, **context):
    # *extra_values captures any extra positional args as a tuple
    # **context captures any named metadata as a dictionary
    extras = ', '.join(str(v) for v in extra_values)  # join any extra positional data
    meta = ' | '.join(f'{k}={v}' for k, v in context.items())  # format key-value metadata
    output = f'[{level}] {message}'
    if extras:
        output += f' | extra: {extras}'
    if meta:
        output += f' | {meta}'
    print(output)


# Call with just the basics
log_flexible('Server started', 'INFO')

# Call with extra positional context
log_flexible('Retrying connection', 'WARN', 'attempt_2', 'pool_B')

# Call with named metadata — no signature change needed
log_flexible('User login failed', 'ERROR', user_id=4821, request_id='req-9f3a')

# Call with both extra positional and named metadata
log_flexible('Payment processed', 'INFO', 'stripe', amount=99.99, currency='USD')
▶ Output
[INFO] Server started
[WARN] Retrying connection | extra: attempt_2, pool_B
[ERROR] User login failed | user_id=4821 | request_id=req-9f3a
[INFO] Payment processed | extra: stripe | amount=99.99 | currency=USD
🔥
The Design Signal:Using *args signals 'order matters, labels don't.' Using **kwargs signals 'labels matter, order doesn't.' Choose based on what your caller's data actually looks like.

How *args and **kwargs Actually Work Under the Hood

The asterisks aren't magic keywords — they're unpacking operators. When Python sees args in a function definition, it's an instruction: 'collect all remaining positional arguments into a tuple and bind that tuple to the name args.' The name itself is just a convention. You could write toppings or *scores and it would work identically.

Similarly, **kwargs says: 'collect all remaining keyword arguments into a dictionary.' The double asterisk means 'key-value pairs,' not just 'things.'

This matters because a tuple and a dictionary behave differently. You iterate over args with a simple for loop. You iterate over *kwargs with .items() to get both the key and the value. You can also use len(), slicing, and unpacking on args just like any tuple. kwargs gives you .get(), .keys(), and the full dict API.

The order of parameters in a function signature is strict: regular positional params first, then args, then keyword-only params, then *kwargs. Breaking this order raises a SyntaxError immediately.

args_kwargs_internals.py · PYTHON
123456789101112131415161718192021222324252627282930
# Demonstrating what *args and **kwargs actually ARE at runtime
def inspect_arguments(*measurements, label='Reading', **sensor_data):
    # *measurements is a plain Python TUPLE — not some special object
    print(f'Type of measurements: {type(measurements)}')
    print(f'Measurements received: {measurements}')
    print(f'Number of readings: {len(measurements)}')

    # **sensor_data is a plain Python DICT
    print(f'Type of sensor_data: {type(sensor_data)}')
    print(f'Sensor metadata: {sensor_data}')
    print(f'Label: {label}')
    print('---')

    # Iterate over args just like any tuple
    for i, reading in enumerate(measurements):
        print(f'  Reading {i + 1}: {reading}')

    # Iterate over kwargs just like any dict
    for key, value in sensor_data.items():
        print(f'  Metadata — {key}: {value}')


# 'label' is a keyword-only param sitting between *args and **kwargs
inspect_arguments(23.4, 25.1, 22.8, label='Temperature', unit='Celsius', location='rack-3')

print('\n--- Unpacking a list INTO *args at call time ---')
# The * operator at CALL time unpacks an iterable into positional args
temperature_readings = [19.0, 20.5, 21.3]
metadata = {'unit': 'Celsius', 'sensor_id': 'T-07'}
inspect_arguments(*temperature_readings, **metadata)  # unpacking at call site
▶ Output
Type of measurements: <class 'tuple'>
Measurements received: (23.4, 25.1, 22.8)
Number of readings: 3
Type of sensor_data: <class 'dict'>
Sensor metadata: {'unit': 'Celsius', 'location': 'rack-3'}
Label: Temperature
---
Reading 1: 23.4
Reading 2: 25.1
Reading 3: 22.8
Metadata — unit: Celsius
Metadata — location: rack-3

--- Unpacking a list INTO *args at call time ---
Type of measurements: <class 'tuple'>
Measurements received: (19.0, 20.5, 21.3)
Number of readings: 3
Type of sensor_data: <class 'dict'>
Sensor metadata: {'unit': 'Celsius', 'sensor_id': 'T-07'}
Label: Reading
---
Reading 1: 19.0
Reading 2: 20.5
Reading 3: 21.3
Metadata — unit: Celsius
Metadata — sensor_id: T-07
⚠️
Pro Tip:You can use * and ** at the call site too — not just the definition. `func(*my_list, **my_dict)` unpacks them into the call. This is how you forward arguments between functions without touching them.

The Pattern That Powers Real Frameworks: Decorator Forwarding

Here's where args and *kwargs stop being a curiosity and become genuinely essential. Decorators — the @something syntax you see on Flask routes, Django views, and test functions — work by wrapping one function inside another. The wrapper needs to call the original function. But the wrapper doesn't know what arguments the original takes.

This is the single most important real-world use case: writing a wrapper function that forwards every argument to the inner function without caring what those arguments are.

Without args and *kwargs, you'd have to write a different decorator for every possible function signature. With them, you write one decorator that works universally. This is also the pattern behind Python's functools.wraps, unittest.mock.patch, and virtually every middleware system you'll encounter in production code.

Once you understand this pattern, reading framework source code stops feeling like magic and starts feeling like recognizable building blocks.

decorator_forwarding_pattern.py · PYTHON
1234567891011121314151617181920212223242526272829303132333435363738394041424344
import time
import functools


# A universal timing decorator that works on ANY function
# It doesn't care how many args the wrapped function takes
def measure_execution_time(func):
    @functools.wraps(func)  # preserves the original function's name and docstring
    def wrapper(*args, **kwargs):  # capture EVERYTHING the caller passes
        start_time = time.perf_counter()
        # Forward ALL captured arguments to the real function, untouched
        result = func(*args, **kwargs)
        end_time = time.perf_counter()
        duration_ms = (end_time - start_time) * 1000
        print(f'[TIMER] {func.__name__} completed in {duration_ms:.3f}ms')
        return result  # always return the original result
    return wrapper


@measure_execution_time
def fetch_user_profile(user_id, include_preferences=False):
    """Simulates a database lookup for a user profile."""
    time.sleep(0.05)  # simulate I/O delay
    profile = {'user_id': user_id, 'name': 'Alex Rivera'}
    if include_preferences:
        profile['theme'] = 'dark'
    return profile


@measure_execution_time
def calculate_order_total(item_prices, discount_code=None, tax_rate=0.08):
    """Simulates a pricing calculation."""
    subtotal = sum(item_prices)
    if discount_code == 'SAVE10':
        subtotal *= 0.90  # apply 10% discount
    return subtotal * (1 + tax_rate)


# The SAME decorator works on both functions with completely different signatures
profile = fetch_user_profile(user_id=1042, include_preferences=True)
print(f'Profile: {profile}\n')

total = calculate_order_total([29.99, 14.99, 49.99], discount_code='SAVE10', tax_rate=0.10)
print(f'Order total: ${total:.2f}')
▶ Output
[TIMER] fetch_user_profile completed in 50.412ms
Profile: {'user_id': 1042, 'name': 'Alex Rivera', 'theme': 'dark'}

[TIMER] calculate_order_total completed in 0.003ms
Order total: $103.92
🔥
Interview Gold:When asked 'why use *args/**kwargs in a decorator?', say: 'Because the wrapper function must be a transparent proxy — it can't know the wrapped function's signature at write time, so it captures and forwards everything.' That answer gets you hired.

When NOT to Use *args and **kwargs

Using args and *kwargs everywhere is a code smell, not a sign of skill. If your function always takes exactly two user IDs to compare, define it that way. Explicit signatures are self-documenting — they tell the caller exactly what's expected. IDEs can autocomplete them. Type checkers can validate them.

**kwargs in particular can hide bugs. If you misspell a keyword argument, Python won't raise a TypeError — it'll just silently add the misspelled key to the dict and your logic will skip it. With explicit parameters, Python catches the typo immediately.

The right time to use them: when you're genuinely wrapping or forwarding calls (decorators, middleware), when you're building a function that is intentionally variadic by design (like a custom print or log function), or when you're writing a base class method that subclasses will extend with their own signatures. The wrong time: when you're just being lazy about writing out three parameters.

when_to_use_kwargs.py · PYTHON
1234567891011121314151617181920212223242526272829303132333435
# WRONG: Using **kwargs when you know exactly what you need
# The caller has no idea what keys are valid — bugs hide silently
def create_user_bad(**kwargs):
    # If caller passes 'usrname' instead of 'username', we'll never know
    username = kwargs.get('username', 'anonymous')  # typo in caller = silent bug
    email = kwargs.get('email', '')
    return {'username': username, 'email': email}


# RIGHT: Explicit parameters when the interface is known and fixed
def create_user_good(username, email, is_admin=False):
    # Caller KNOWS exactly what to pass. Typos raise TypeError immediately.
    return {'username': username, 'email': email, 'is_admin': is_admin}


# RIGHT: **kwargs makes sense here — forwarding to a third-party library
def create_database_connection(host, port, **driver_options):
    # We know host and port (our API), but driver_options belong to the DB driver
    # We don't want to maintain a mirror of the driver's full option set
    print(f'Connecting to {host}:{port} with options: {driver_options}')
    # In real code: return driver.connect(host=host, port=port, **driver_options)


# Demonstrate the silent bug in the bad version
result = create_user_bad(usrname='alex', email='alex@example.com')  # typo: 'usrname'
print(f'Bad result (typo went unnoticed): {result}')  # username defaults to 'anonymous'

# Explicit version catches the error immediately
try:
    create_user_good(usrname='alex', email='alex@example.com')  # same typo
except TypeError as error:
    print(f'Good result (typo caught instantly): {error}')

# Forwarding pattern — valid use of **kwargs
create_database_connection('db.prod.internal', 5432, timeout=30, ssl=True, pool_size=10)
▶ Output
Bad result (typo went unnoticed): {'username': 'anonymous', 'email': 'alex@example.com'}
Good result (typo caught instantly): create_user_good() got an unexpected keyword argument 'usrname'
Connecting to db.prod.internal:5432 with options: {'timeout': 30, 'ssl': True, 'pool_size': 10}
⚠️
Watch Out:**kwargs swallows typos silently. If your function receives user-provided keys and processes them with .get(), a misspelled key just returns None instead of raising an error. Use explicit parameters whenever the interface is under your control.
Feature / Aspect*args**kwargs
What it collectsExtra positional argumentsExtra keyword arguments
Python type at runtimetupledict
Argument style at call sitefunc(1, 2, 3)func(a=1, b=2, c=3)
How to iteratefor item in argsfor key, value in kwargs.items()
Order preserved?Yes — insertion orderYes — insertion order (Python 3.7+)
Unpack into callfunc(*my_list)func(**my_dict)
Position in signatureAfter regular paramsAlways last
Primary use caseVariadic positional data (scores, prices)Named config / metadata forwarding
Typo detectionN/A — positional, no namesSilent — typos become valid keys
Can be used alone?YesYes

🎯 Key Takeaways

  • args is a tuple at runtime — iterate it, slice it, len() it like any tuple. *kwargs is a dict — use .items(), .get(), and .keys() on it.
  • The universal decorator pattern (wrapper(args, kwargs) → func(args, **kwargs)) is the single most important real-world use of these features. Master it.
  • kwargs silently swallows typos — if your function's interface is fixed and known, use explicit named parameters instead. Save kwargs for genuine forwarding scenarios.
  • The and * operators work in BOTH directions: in function definitions they collect arguments into tuple/dict; at the call site they unpack tuple/dict back into arguments. Same symbol, opposite direction.

⚠ Common Mistakes to Avoid

  • Mistake 1: Putting *kwargs before args in a function signature — causes a SyntaxError: 'invalid syntax' immediately — fix it by always following the order: regular params → args → keyword-only params → *kwargs. Python enforces this strictly.
  • Mistake 2: Mutating the kwargs dict inside the function and expecting it to affect the caller's dict — it won't, because **kwargs creates a new dict from the caller's keyword arguments. The caller's original variables are untouched. If you need to pass mutations back, return the modified dict explicitly.
  • Mistake 3: Passing a dict as a positional argument instead of unpacking it — calling func({'key': 'value'}) when you mean func(*{'key': 'value'}) sends the whole dict as one positional arg into args, not as keyword args. The symptom is args = ({'key': 'value'},) and kwargs is empty. Always use ** to unpack dicts into kwargs.

Interview Questions on This Topic

  • QCan you walk me through how you'd write a decorator that logs the arguments and return value of any function it wraps, regardless of that function's signature?
  • QWhat's the difference between defining a function with *args and calling a function with the * operator? They look similar — are they the same thing?
  • QIf I call `my_func(**{'timeout': 30, 'retries': 3})` and my function signature is `def my_func(*args, **kwargs)`, what will args and kwargs contain? What if I accidentally pass a positional argument at the same time?

Frequently Asked Questions

Do I have to name them 'args' and 'kwargs'?

No — the names are pure convention. The operators are and . You can write def func(scores, **player_data) and it works identically. That said, stick to args and kwargs unless you have a specific reason not to — every Python developer recognises them instantly.

Can I use *args and **kwargs together with regular parameters?

Yes, but the order is strict: regular positional params come first, then args, then any keyword-only params (params defined after args), then *kwargs. Breaking this order raises a SyntaxError. Example: def func(name, age, scores, active=True, **metadata) is valid.

Why does modifying kwargs inside my function not change the original dict I passed in?

Because kwargs doesn't receive your dictionary — it receives a new dictionary built from the keyword arguments at the call site. The double-star operator unpacks key-value pairs; it doesn't pass a reference to your original dict. If you pass my_config, modifying kwargs inside the function leaves my_config unchanged.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousFunctions in PythonNext →Lambda Functions in Python
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged