Skip to contentSkip to navigation
Module 05 / Lesson 03intermediate20 min read

Variadic Arguments (*args & **kwargs)

Accept arbitrary positional and keyword parameters, unpack iterables, and enforce keyword-only boundaries.

Prerequisites: function-arguments
Architecture: Vectorcall Protocol & Dynamic Packaging

01 / Mental Model: Universal Adapters & Unpacking Levers

Think of variadic arguments as universal adapters on a function's intake manifold. When you do not know in advance how many inputs a caller will supply, *args acts as a funnel scooping up surplus unnamed positional arguments into an immutable tuple. Simultaneously, **kwargs acts as a labeled sorting bin collecting arbitrary named arguments into a mutable dictionary. At call sites, the asterisk acts in reverse as an unpacking lever, scattering elements across parameters.

Interactive Variadic Visualizer
Caller: log_metrics('server_1', 42.1, 98.4, 15.0, region='us-east', env='prod')

Function Intake Processing:
  1. 'server_1'                 ─────────► target_id (fixed positional)
  2. 42.1, 98.4, 15.0           ──*args──► args = (42.1, 98.4, 15.0) [PyTupleObject]
  3. region='us-east', env='prod' ─**kwargs─► kwargs = {'region': 'us-east', 'env': 'prod'} [PyDictObject]

*args gathers all leftover positional arguments into a tuple. **kwargs collects all keyword arguments not matched by fixed parameters into a dictionary.

02 / Syntax Anatomy: Variadics, Keyword-Only Delimiters & Forwarding

#   [1]          [2]       [3]           [4]
    def execute(command, *arguments, verbose=False, **env_vars):
        """Execute system command with dynamic flags and environment mapping."""
        # arguments is tuple ()
        # env_vars is dict {}
[1] command:

Mandatory leading positional parameter.

[2] *arguments:

Packs arbitrary surplus positional parameters into an immutable tuple.

[3] verbose=False:

Keyword-only parameter (follows *arguments). Must be passed by name.

[4] **env_vars:

Captures all remaining keyword arguments into a mutable dictionary.

03 / Under the Hood: CPython Flags (CO_VARARGS) & Vectorcall Protocol

CPython Execution Sequence
Stage 1: Bytecode Code Object Flags (co_flags)

When CPython compiles a function containing *args, it activates the bit flag CO_VARARGS (0x0004) in func.__code__.co_flags. When it encounters **kwargs, it sets CO_VARKEYWORDS (0x0008). These flags signal to the frame allocator that dynamic argument packaging is enabled.

flags = func.__code__.co_flags
is_varargs = bool(flags & 0x0004)       # CO_VARARGS
is_varkeywords = bool(flags & 0x0008)   # CO_VARKEYWORDS

04 / Comparison Matrix: Variadic Forms & Unpacking Behaviors

ParameterCaptured TypeMutabilityOrder ConstraintEmpty Call Value
*argstupleImmutablePrecedes keyword-only & **kwargs()
**kwargsdictMutableMust be absolute last{}

05 / Syntax Reference: Variadic Signatures & Call Unpacking

Syntax FormContextType Received / SentConstraintsExample
*args in Definitiondef f(*args):Receives tuple ()Only one *args allowed; must follow standard parameters.f(1, 2) -> args = (1, 2)
**kwargs in Definitiondef f(**kwargs):Receives dict {}Must be the very last parameter in the signature.f(a=1, b=2) -> kwargs = {'a': 1, 'b': 2}
Bare * in Definitiondef f(a, *, b):Delimiter (no variable)Forces all subsequent parameters to be keyword-only.f(1, b=2) valid; f(1, 2) raises TypeError
*iterable at Call Sitef(*items)Unpacks any iterableExpanded into positional arguments in order.f(*[10, 20]) -> f(10, 20)
**dict at Call Sitef(**options)Unpacks mappingAll keys must be strings matching parameter names.f(**{'x': 1}) -> f(x=1)

06 / Core Code Walkthroughs: 7 Variadic Design Patterns

Variadic Positional Arguments (*args) for Dynamic Aggregation

Using *args to accept an arbitrary quantity of positional arguments packed cleanly into an immutable tuple.

Variadic Positional Arguments (*args) for Dynamic Aggregation Run cleanly to earn +1 XP
def compute_product(*numbers):
    """Multiply an arbitrary sequence of numbers together."""
    print('args type:', type(numbers), 'length:', len(numbers))
    product = 1
    for n in numbers:
        product *= n
    return product

print('Product of 3:', compute_product(2, 3, 4))
print('Product of 5:', compute_product(1, 2, 3, 4, 5))
print('Product of 0 args:', compute_product())
Run cleanly to earn +1 XP: Select Run (or edit code first to experiment)! Runs that end in an error earn no XP.

07 / Interactive Playground: Universal Decorator Forwarding

Modify the code below to see how universal argument forwarding (*args, **kwargs) intercepts calls without altering the original function's signature.

Run & Experiment with *args, **kwargs Forwarding
import time

def log_execution(func):
    """Universal decorator wrapping arbitrary signatures seamlessly."""
    def wrapper(*args, **kwargs):
        print(f"-> Calling {func.__name__} with:")
        print(f"   Positional args ({len(args)}): {args}")
        print(f"   Keyword args ({len(kwargs)}): {kwargs}")
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = (time.perf_counter() - start) * 1000
        print(f"<- {func.__name__} returned {result} in {elapsed:.3f}ms")
        return result
    return wrapper

@log_execution
def calculate_tax(subtotal, *, rate=0.08, discount=0.0):
    effective = max(0.0, subtotal - discount)
    return round(effective * (1 + rate), 2)

final_bill = calculate_tax(100.0, discount=10.0, rate=0.10)
print("Final Bill:", final_bill)

08 / Common Pitfalls & Anti-Patterns

🟢 Beginner Pitfall: Placing **kwargs Before Other Parameters in Definition

**kwargs must strictly be the final parameter in any Python function header. Placing any parameter—positional, default, *args, or keyword-only—after **kwargs causes a compile-time SyntaxError: invalid syntax.

❌ Anti-Pattern:
# SyntaxError: invalid syntax
# def bad_func(**kwargs, *args):
#     pass
✅ Idiomatic Solution:
# Correct: *args precedes **kwargs
def good_func(*args, **kwargs):
    pass

🟢 Beginner Pitfall: Unpacking a Dictionary with a Single Asterisk (*dict)

Preceding a dictionary with a single asterisk (*d) unpacks only its KEYS as positional arguments, not its key-value pairs! To unpack a dictionary into keyword arguments, you must use the double asterisk (**d).

❌ Anti-Pattern:
def greet(first, last):
    return f'{first} {last}'

names = {'first': 'Grace', 'last': 'Hopper'}
# *names unpacks the keys: greet('first', 'last')!
print('Single *:', greet(*names))
✅ Idiomatic Solution:
def greet(first, last):
    return f'{first} {last}'

names = {'first': 'Grace', 'last': 'Hopper'}
# **names unpacks keyword arguments: greet(first='Grace', last='Hopper')
print('Double **:', greet(**names))

🟡 Intermediate Pitfall: Passing Duplicate Argument Values via Both Positional and **kwargs

If a parameter is filled positionally and also included inside an unpacked **kwargs dictionary, Python raises TypeError: function() got multiple values for keyword argument.

❌ Anti-Pattern:
def set_point(x, y):
    return x, y

params = {'x': 10, 'y': 20}
try:
    # 5 fills x positionally, then **params attempts to fill x again!
    set_point(5, **params)
except TypeError as e:
    print('Collision error:', e)
✅ Idiomatic Solution:
def set_point(x, y):
    return x, y

params = {'y': 20}
print('Clean call:', set_point(5, **params))

🟡 Intermediate Pitfall: Attempting In-Place Mutation on args Inside Function

Because *args packs arguments into a tuple (PyTupleObject), it is strictly immutable. Attempting to call args.append() or args[0] = x raises AttributeError or TypeError. If you need a mutable list, you must explicitly cast it with list(args).

❌ Anti-Pattern:
def process_items(*args):
    try:
        args.append('extra')  # Raises AttributeError: 'tuple' object has no attribute 'append'
    except AttributeError as e:
        print('Mutation error:', e)

process_items(1, 2)
✅ Idiomatic Solution:
def process_items(*args):
    items_list = list(args)
    items_list.append('extra')
    return items_list

print('Result:', process_items(1, 2))

09 / Edge Cases: Generators, Overlapping Dictionaries & Non-String Keys

1

Unpacking Generators with *gen

Unpacking a generator expression (*(x*2 for x in range(3))) fully exhausts the generator immediately during the function call, converting its yielded items into arguments.

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

gen = (x * 10 for x in range(1, 4))
print('From generator:', show_three(*gen))
Output: From generator: 60
2

Unpacking Dictionaries with Non-String Keys

Python keyword arguments must be valid identifier strings. Attempting to unpack a dictionary with non-string keys (such as integers) via **d raises TypeError: keywords must be strings.

def test_kw(**kwargs):
    return kwargs

bad_dict = {100: 'val'}
try:
    test_kw(**bad_dict)
except TypeError as exc:
    print('Key error:', exc)
Output: Key error: test_kw() keywords must be strings
3

Duplicate Keys Across Multiple **dict Unpackings

When unpacking multiple dictionaries in the same call (e.g. f(**d1, **d2)), if both dictionaries contain the same key, the later dictionary overrides the earlier one without error.

def inspect_merged(**kwargs):
    return kwargs

base = {'color': 'red', 'size': 10}
override = {'color': 'blue'}
print('Merged kwargs:', inspect_merged(**base, **override))
Output: Merged kwargs: {'color': 'blue', 'size': 10}
4

Single-Element Tuple Unpacking

Unpacking a 1-element tuple *(x,) passes exactly 1 positional argument. Notice the mandatory trailing comma in (x,).

def echo(val):
    return val

single_tuple = (42,)
print('Unpacked single:', echo(*single_tuple))
Output: Unpacked single: 42

10 / Memory & Performance Trade-Offs

Tuple & Dict Heap Allocation Costs

Heap Churn

Every call to a function with *args allocates a PyTupleObject, and **kwargs allocates a PyDictObject on the heap. In performance-critical micro-benchmarks, explicitly declaring named parameters is faster.

Vectorcall Call-Site Optimization (Python 3.12)

Fast Stack Array

In CPython 3.12, the vectorcall protocol optimizes *args unpacking by populating an existing array buffer when possible, minimizing intermediate object creation.

Universal Decorator Wrapper Overhead

Invocation Latency

Using def wrapper(*args, **kwargs): func(*args, **kwargs) introduces slight allocation and argument-parsing latency compared to a direct call, though PyPy and specialized JITs often inline this pattern.

Keyword Hashing in **kwargs Prologue

String Hash Cost

**kwargs dictionary construction hashes every keyword string. For calls with many keyword arguments, this string hashing cost dominates function prologue time.

11 / Step-by-Step Problem Solving: Structuring Variadic Functions

Step 1: Anchor Mandatory Parameters

Every robust function begins with explicit parameters for its core non-negotiable inputs. Never swallow required parameters inside *args.

def emit_metric(metric_name: str):
    pass

12 / Real-World Applications & Production Architecture

01

Universal Decorator & Middleware Forwarding

Web framework middlewares, telemetry monitors, and retry decorators rely on def wrapper(*args, **kwargs) to intercept, time, and log function calls without altering the underlying function's argument signature.

02

Dynamic SQL and Query Building

ORM query engines (e.g. Django ORM, SQLAlchemy) accept arbitrary keyword arguments in methods like filter_by(**conditions), dynamically translating keyword names into column filter clauses.

03

Configurable Component Factories

Plugin architectures and UI component factories use *args, **kwargs to pass variable configuration parameters through abstract factories down to concrete class constructors.

13 / Interactive Quiz Booster (17 Questions)

Score: 0 / 17
Question 1 of 17
What data type does *args pack positional arguments into inside a function?
easy
Question 2 of 17
What does this print?
def add_all(*args):
    return sum(args)
print(add_all(1, 2, 3, 4))
easy
Question 3 of 17
What data type does **kwargs pack keyword arguments into inside a function?
easy
Question 4 of 17
What does this print?
def greet(**kwargs):
    return kwargs.get("name", "Guest")
print(greet(name="Ali"))
easy
Question 5 of 17
What does this print?
def show(a, b, *rest):
    return len(rest)
print(show(1, 2, 3, 4, 5))
medium
Question 6 of 17
What does the single asterisk * do when calling a function with a list func(*items)?
medium
Question 7 of 17
Which function signature contains a syntax error according to Python argument ordering rules?
medium
Question 8 of 17
What does this print?
def multiply(x, y):
    return x * y
pair = [4, 5]
print(multiply(*pair))
medium
Question 9 of 17
Which syntax marks parameter 'mode' as keyword-only in Python?
hard
Question 10 of 17
What does this print?
def config(**kwargs):
    return len(kwargs)
print(config(debug=True, port=8080))
hard
Question 11 of 17
What bytecode flags indicate the presence of *args and **kwargs on a Python code object?
hard
Question 12 of 17
What does calling `func(*{'a': 1, 'b': 2})` with a single asterisk pass to `func`?
medium
Question 13 of 17
What happens if you try to modify `args` inside a function with `args[0] = 99`?
medium
Question 14 of 17
What happens when unpacking multiple dictionaries with overlapping keys like `f(**d1, **d2)`?
hard
Question 15 of 17
What does this print?
def test(a, *args, b=10):
    return a, args, b
print(test(1, 2, 3, b=20))
medium
Question 16 of 17
Why is `def wrapper(*args, **kwargs): return func(*args, **kwargs)` the standard decorator signature?
medium
Question 17 of 17
What happens if a dictionary passed to `**kwargs` has integer keys like `{0: 'a'}`?
hard

14 / Guided Practice Labs & Test Suites

Lab 1

Chained Functional Pipeline with *functions

easy

Write a function pipe_transform(initial_value, *functions) that applies each function in *functions sequentially to initial_value, passing the result of each step to the next function. Return the final result.

Hint: Loop over functions: result = fn(result) and return result.
Starter Workspace:
Starter: Chained Functional Pipeline with *functions
def pipe_transform(initial_value, *functions):
    # Apply each function in order
    pass
Verification Test Assertions:
assert pipe_transform('  hello world  ', str.strip, str.title) == 'Hello World'
assert pipe_transform(5, lambda x: x * 2, lambda x: x + 10) == 20
assert pipe_transform(42) == 42
Lab 2

HTML Tag Generator with **attributes

medium

Write a function html_tag(tag, content='', **attrs) that returns an HTML element string. Attributes should be formatted as key="value" sorted alphabetically by key. Example: html_tag('a', 'Home', href='/', id='nav') -> '<a href="/" id="nav">Home</a>'.

Hint: Sort attrs.items(), build formatted string of key="val", and enclose content in opening and closing tags.
Starter Workspace:
Starter: HTML Tag Generator with **attributes
def html_tag(tag, content='', **attrs):
    # Return formatted HTML string with sorted attributes
    pass
Verification Test Assertions:
assert html_tag('a', 'Home', href='/', id='nav') == '<a href="/" id="nav">Home</a>'
assert html_tag('p', 'Hello') == '<p>Hello</p>'
assert html_tag('div', '', cls='container') == '<div cls="container"></div>'
Lab 3

Tiered Configuration Merger

hard

Write a function merge_configs(*configs, **overrides) that takes an arbitrary number of configuration dictionaries (*configs) and merges them in order, then applies any keyword overrides (**overrides) with highest precedence. Return the resulting merged dictionary.

Hint: Initialize merged = {}, iterate over configs updating merged, then update with overrides.
Starter Workspace:
Starter: Tiered Configuration Merger
def merge_configs(*configs, **overrides):
    # Merge configs and apply overrides
    pass
Verification Test Assertions:
c1 = {'host': '127.0.0.1', 'port': 80}
c2 = {'port': 8080, 'debug': False}
m = merge_configs(c1, c2, debug=True, log_level='INFO')
assert m == {'host': '127.0.0.1', 'port': 8080, 'debug': True, 'log_level': 'INFO'}
assert merge_configs(a=1) == {'a': 1}

15 / Debugging Scenarios: Production Post-Mortems

Case 01

TypeError: got multiple values for keyword argument 'timeout'

Symptom:

An API client call fetch_data('https://api.io', 30, **default_options) threw a runtime error.

Root Cause:

The second positional argument 30 filled parameter 'timeout', and default_options also contained 'timeout': 60.

Resolution:

Remove the redundant positional argument or strip 'timeout' from default_options before unpacking.

Case 02

Silent Misconfiguration Due to Single Asterisk on Dictionary

Symptom:

A database helper connect(*db_credentials) failed with missing host/port errors instead of authenticating.

Root Cause:

The author wrote *db_credentials instead of **db_credentials. Only the dictionary keys ('host', 'port') were unpacked positionally.

Resolution:

Use **db_credentials to unpack key-value pairs as named keyword arguments.

16 / Best Practices, Key Takeaways & Quick Cheatsheet

Engineering Best Practices

Always put **kwargs as the very last parameter in any function signature.
Use *args when writing mathematical or aggregative functions that operate on an arbitrary count of operands.
Use **kwargs for extensible options dictionaries or when wrapping functions in decorators.
Use bare * in parameter lists to mandate keyword-only arguments for optional boolean flags and configuration settings.
Be careful when unpacking dictionaries: *dict unpacks keys only, while **dict unpacks keyword arguments.
Avoid overusing *args and **kwargs for ordinary domain functions where explicit named parameters provide clearer contracts and IDE autocompletion.

Core Mental Pillars

Pillar 1: *args collects surplus positional arguments into an immutable tuple.
Pillar 2: **kwargs collects surplus keyword arguments into a mutable dictionary.
Pillar 3: A bare * in the parameter signature establishes a barrier requiring all subsequent parameters to be keyword-only.
Pillar 4: At call sites, * unpacks iterables into positional arguments, while ** unpacks dictionaries into keyword arguments.
Pillar 5: The full signature order is: positional-only (/), standard, defaults, *args, keyword-only (*), **kwargs.

Quick Reference Cheatsheet

Variadic Positionaldef fn(*args):

Gathers surplus positional arguments into an immutable tuple.

Variadic Keyworddef fn(**kwargs):

Captures surplus keyword arguments into a mutable dictionary.

Call-Site * Unpackfn(*items)

Scatters sequence elements across positional parameter slots.

Call-Site ** Unpackfn(**mapping)

Broadcasts dictionary key-value pairs into keyword arguments.

Frequently Asked Questions

Worked example

Calculate geometric mean or product of any count of input numbers using *args.

Example
def prod(*nums):
    total = 1
    for n in nums: total *= n
    return total
print(prod(2, 3, 4))

How it works, step by step

  1. *nums collects all positional arguments into a tuple.

Your notes

Sign in to keep private notes alongside this lesson.

Sign in to take notes

Variadic Arguments (*args & **kwargs) practice

Sign-in required

Write a function sum_and_tag(tag, *numbers) that sums all numbers passed in *numbers and prints [{tag}] Total: {total}. Call sum_and_tag("Math", 10, 20, 30).

Coding Practice is Locked

Sign in to access the interactive Python editor, write and run code checks against automated tests, use hints, and earn XP.

Practice quiz

Check yourself

15 questions with explanations after submission, so you can correct course as you go.

Variadic Arguments (*args & **kwargs) quiz

Locked

You must be signed in to take this quiz. Sign in to test your understanding, track your answers, and earn XP.

Lesson incomplete

Not completed

Create an ID or sign in to save lesson completion across devices.

Create ID to save
Module lesson progress0/6 complete
Module cheat sheet