Variadic Arguments (*args & **kwargs)
Accept arbitrary positional and keyword parameters, unpack iterables, and enforce keyword-only boundaries.
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.
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 {}Mandatory leading positional parameter.
Packs arbitrary surplus positional parameters into an immutable tuple.
Keyword-only parameter (follows *arguments). Must be passed by name.
Captures all remaining keyword arguments into a mutable dictionary.
03 / Under the Hood: CPython Flags (CO_VARARGS) & Vectorcall Protocol
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
| Parameter | Captured Type | Mutability | Order Constraint | Empty Call Value |
|---|---|---|---|---|
| *args | tuple | Immutable | Precedes keyword-only & **kwargs | () |
| **kwargs | dict | Mutable | Must be absolute last | {} |
05 / Syntax Reference: Variadic Signatures & Call Unpacking
| Syntax Form | Context | Type Received / Sent | Constraints | Example |
|---|---|---|---|---|
| *args in Definition | def f(*args): | Receives tuple () | Only one *args allowed; must follow standard parameters. | f(1, 2) -> args = (1, 2) |
| **kwargs in Definition | def 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 Definition | def 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 Site | f(*items) | Unpacks any iterable | Expanded into positional arguments in order. | f(*[10, 20]) -> f(10, 20) |
| **dict at Call Site | f(**options) | Unpacks mapping | All 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.
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())
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.
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.
# SyntaxError: invalid syntax # def bad_func(**kwargs, *args): # pass
# 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).
def greet(first, last):
return f'{first} {last}'
names = {'first': 'Grace', 'last': 'Hopper'}
# *names unpacks the keys: greet('first', 'last')!
print('Single *:', greet(*names))
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.
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)
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).
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)
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
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))
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)
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))
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))
10 / Memory & Performance Trade-Offs
Tuple & Dict Heap Allocation Costs
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)
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
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
**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
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):
pass12 / Real-World Applications & Production Architecture
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.
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.
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)
def add_all(*args):
return sum(args)
print(add_all(1, 2, 3, 4))def greet(**kwargs):
return kwargs.get("name", "Guest")
print(greet(name="Ali"))def show(a, b, *rest):
return len(rest)
print(show(1, 2, 3, 4, 5))def multiply(x, y):
return x * y
pair = [4, 5]
print(multiply(*pair))def config(**kwargs):
return len(kwargs)
print(config(debug=True, port=8080))def test(a, *args, b=10):
return a, args, b
print(test(1, 2, 3, b=20))14 / Guided Practice Labs & Test Suites
Chained Functional Pipeline with *functions
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.
def pipe_transform(initial_value, *functions):
# Apply each function in order
pass
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
HTML Tag Generator with **attributes
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>'.
def html_tag(tag, content='', **attrs):
# Return formatted HTML string with sorted attributes
pass
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>'
Tiered Configuration Merger
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.
def merge_configs(*configs, **overrides):
# Merge configs and apply overrides
pass
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
TypeError: got multiple values for keyword argument 'timeout'
An API client call fetch_data('https://api.io', 30, **default_options) threw a runtime error.
The second positional argument 30 filled parameter 'timeout', and default_options also contained 'timeout': 60.
Remove the redundant positional argument or strip 'timeout' from default_options before unpacking.
Silent Misconfiguration Due to Single Asterisk on Dictionary
A database helper connect(*db_credentials) failed with missing host/port errors instead of authenticating.
The author wrote *db_credentials instead of **db_credentials. Only the dictionary keys ('host', 'port') were unpacked positionally.
Use **db_credentials to unpack key-value pairs as named keyword arguments.
16 / Best Practices, Key Takeaways & Quick Cheatsheet
Engineering Best Practices
Core Mental Pillars
Quick Reference Cheatsheet
def fn(*args):Gathers surplus positional arguments into an immutable tuple.
def fn(**kwargs):Captures surplus keyword arguments into a mutable dictionary.
fn(*items)Scatters sequence elements across positional parameter slots.
fn(**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.
def prod(*nums):
total = 1
for n in nums: total *= n
return total
print(prod(2, 3, 4))
How it works, step by step
- *nums collects all positional arguments into a tuple.
Variadic Arguments (*args & **kwargs) practice
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
LockedYou must be signed in to take this quiz. Sign in to test your understanding, track your answers, and earn XP.
Lesson incomplete
Not completedCreate an ID or sign in to save lesson completion across devices.