Advanced Argument Unpacking: *args and **kwargs in Depth0%
Default vs Keyword Arguments

Advanced Argument Unpacking: *args and **kwargs in Depth

Intermediate14 min readUpdated: 2026-09-12
Study Materials

Advanced Argument Unpacking: args and *kwargs in Depth

In beginner Python, you learned that *args collects extra positional arguments into a tuple, while **kwargs collects extra keyword arguments into a dictionary.

In intermediate software architecture, mastering *args and **kwargs is crucial for creating flexible decorator wrappers, forwarding arguments across inheritance hierarchies, enforcing keyword-only constraints, and designing robust, extensible APIs.


Real-World Analogy: The Indian Wedding Buffet & Dietary Notes

Imagine the catering operations at a grand Indian wedding banquet:

Visual Architecture Blueprint
+-------------------------------------------------------------------------+
|                  INDIAN WEDDING BUFFET CATERING PROTOCOL                |
+-------------------------------------------------------------------------+
|                                                                         |
|  Guest Platter: (*args)                                                 |
|  ──> Gathers an arbitrary tuple of buffet items:                        |
|      ("Paneer Tikka", "Gulab Jamun", "Dal Makhani", "Naan")             |
|                                                                         |
|  Special Dietary Notes Ledger: (**kwargs)                               |
|  ──> Gathers an arbitrary dictionary of key-value preferences:          |
|      {"spicy_level": "Mild", "jain": True, "table_no": 14}              |
|                                                                         |
|  Master Chef Function:                                                  |
|  def prepare_meal(guest_id, *dishes, **special_notes):                  |
|      # Can serve any guest with any number of dishes and notes!         |
|                                                                         |
+-------------------------------------------------------------------------+

Whether a guest requests 2 dishes or 10, with zero notes or 5 special instructions, the chef's function accepts the order seamlessly without crashing or requiring rigid signature changes.


The Golden Order of Function Parameters

Python strictly enforces the order in which parameters can be declared in a function signature:

Output
def func(pos_only, /, standard_arg, *args, kw_only, **kwargs):
▲ ▲ ▲ ▲ ▲
│ │ │ │ └─ Extra Keyword Dict
│ │ │ └─ Must pass as name=val
│ │ └─ Extra Positional Tuple
│ └─ Normal positional or keyword
└─ Positional-Only (Python 3.8+)

Signature Hierarchy Rules:

  1. 1
    Positional-only parameters (before /).
  2. 2
    Standard parameters (positional or keyword).
  3. 3
    *`args** (absorbs remaining positional arguments as a tuple`).
  4. 4
    Keyword-only parameters (placed after *args or a bare *).
  5. 5
    `kwargs** (absorbs remaining named arguments as a dict`).

Packing vs Unpacking: The Dual Role of Asterisks

The asterisks * and ** perform two completely opposite actions depending on where they appear:

Visual Architecture Blueprint
+------------------------------------+------------------------------------+
|  1. PACKING (in function def)      |  2. UNPACKING (at call-site)       |
+------------------------------------+------------------------------------+
|  def log_event(*args, **kwargs):   |  params = [10, 20, 30]             |
|      # Bundles items into:         |  options = {"timeout": 5}          |
|      # args -> tuple               |  send_data(*params, **options)     |
|      # kwargs -> dict              |  # Explodes collections into args! |
+------------------------------------+------------------------------------+

Comprehensive Code Examples

1. Transparent Argument Forwarding in Wrappers

The most common intermediate pattern for *args and **kwargs is transparently passing arguments to another function or class constructor:

Python
import time
 
def audit_logger(target_function):
"""Decorator-style wrapper that intercepts any function call."""
def wrapper(*args, **kwargs):
print(f"[AUDIT] Calling '{target_function.__name__}'")
print(f" Positional arguments received : {args}")
print(f" Keyword arguments received : {kwargs}")
# Unpack and forward parameters transparently
result = target_function(*args, **kwargs)
print(f"[AUDIT] '{target_function.__name__}' returned successfully.\n")
return result
return wrapper
 
@audit_logger
def process_fund_transfer(sender_acc, recipient_acc, amount, *, ifsc_code, remarks="Transfer"):
return {
"status": "SUCCESS",
"txn_id": "TXN998822",
"sender": sender_acc,
"recipient": recipient_acc,
"net_amount": amount,
"ifsc": ifsc_code
}
 
# Execute function
receipt = process_fund_transfer(
"SBIN000101",
"HDFC000452",
25000.0,
ifsc_code="HDFC0001234",
remarks="Vendor Invoice Settlement"
)
print("Transaction Receipt:", receipt)

Expected Output:

Output
[AUDIT] Calling 'process_fund_transfer'
Positional arguments received : ('SBIN000101', 'HDFC000452', 25000.0)
Keyword arguments received : {'ifsc_code': 'HDFC0001234', 'remarks': 'Vendor Invoice Settlement'}
[AUDIT] 'process_fund_transfer' returned successfully.
 
Transaction Receipt: {'status': 'SUCCESS', 'txn_id': 'TXN998822', 'sender': 'SBIN000101', 'recipient': 'HDFC000452', 'net_amount': 25000.0, 'ifsc': 'HDFC0001234'}

2. Enforcing Keyword-Only Arguments with Bare *

To eliminate ambiguities in function calls (e.g. accidentally swapping boolean flags), enforce keyword-only parameters using a bare asterisk *:

Python
# The bare '*' forces all parameters after it to be passed explicitly as keyword arguments
def issue_train_ticket(passenger_name, train_no, *, coach="3A", senior_citizen=False, meal_preference="Veg"):
return {
"passenger": passenger_name,
"train": train_no,
"coach": coach,
"discount": "40% Senior Concession" if senior_citizen else "Standard Fare",
"meal": meal_preference
}
 
# Valid call: positional for mandatory, keywords for options
t1 = issue_train_ticket("Aarav Mehta", "12951", senior_citizen=True, coach="2A")
print("Ticket 1:", t1)
 
# Calling with positional options causes an immediate TypeError:
# issue_train_ticket("Pooja", "12001", "1A", True) # Raises TypeError!

Expected Output:

Output
Ticket 1: {'passenger': 'Aarav Mehta', 'train': '12951', 'coach': '2A', 'discount': '40% Senior Concession', 'meal': 'Veg'}

3. Call-Site Dictionary Merging and Unpacking

In modern Python, the unpacking operators * and ** can merge iterables and dictionaries directly into new objects:

Python
# Merging configuration dictionaries using ** unpacking
base_db_config = {
"host": "localhost",
"port": 5432,
"timeout": 30
}
 
prod_overrides = {
"host": "db.mumbai.msk-prod.internal",
"ssl_mode": "require",
"max_connections": 100
}
 
# Merge: prod_overrides overwrite matching keys from base_db_config
active_config = {**base_db_config, **prod_overrides, "env": "production"}
 
print("Merged Configuration:")
for k, v in active_config.items():
print(f" {k:<16}: {v}")

Expected Output:

Output
Merged Configuration:
host : db.mumbai.msk-prod.internal
port : 5432
timeout : 30
ssl_mode : require
max_connections : 100
env : production

Best Practices & Comparison: Do's and Don'ts

PracticeBad / Error-Prone PatternRecommended Gold Standard
Parameter Orderingdef bad(**kwargs, *args): (SyntaxError)def good(a, b, *args, **kwargs):
OveruseUsing *args, **kwargs on every single functionUse explicit named parameters whenever function arity is known
Keyword ClarityLong lists of confusing booleans: fn(a, True, False, True)Enforce keyword-only: def fn(a, *, log=True, retry=False):
Modifying argsTrying to mutate args (args.append(x))args is an immutable tuple; convert to list if mutation needed
ForwardingDropping kwargs when wrapping functionsForward *args, **kwargs to preserve original signature flexibility

Quick Revision Summary Cheat Sheet

  • In Definitions:
  • *args: Collects extra positional parameters into a tuple.
  • **kwargs: Collects extra named keyword arguments into a dict.
  • At Call-Sites:
  • *iterable: Unpacks elements into individual positional arguments.
  • **dict: Unpacks key-value pairs into individual named keyword arguments.
  • Order of Parameters: (pos_only, /, standard, *args, kw_only, **kwargs).
  • *Bare Asterisk ``:** Marks all subsequent parameters as keyword-only.
  • Dictionary Merging: {**dict_a, **dict_b} merges dictionaries with right-hand precedence.

Multiple Choice Questions

1. In a function definition def demo(*args):, what data structure does args represent inside the function body?

A. List B. Tuple C. Dictionary D. Set Answer: B Explanation: *args packs variable positional arguments into an immutable tuple, not a mutable list.


2. Which function signature violates Python syntax and raises a SyntaxError upon definition?

A. def func(a, *args, b=10, **kwargs): B. def func(a, b, *, debug=True): C. def func(**kwargs, *args): D. def func(*args, **kwargs): Answer: C Explanation: Python syntax mandates that *args must always precede **kwargs. Placing **kwargs before *args results in an immediate SyntaxError: invalid syntax.


3. What does a standalone bare asterisk () do in def process(data, , secure=True)?:

A. Enables pointer arithmetic B. Forces all parameters declared after the asterisk (secure) to be passed exclusively as keyword arguments C. Multiplies data by secure D. Allows infinite positional parameters Answer: B Explanation: A bare asterisk * acts as a delimiter indicating that all subsequent parameters are keyword-only and cannot be supplied positionally.


4. What is the output of the following code snippet?

Python
def multiply(x, y, z):
return x * y * z
 
numbers = [2, 3, 4]
print(multiply(*numbers))

A. multiply([2, 3, 4]) B. 24 C. [2, 3, 4, 2, 3, 4] D. TypeError: multiply() missing 2 required positional arguments Answer: B Explanation: The call-site unpacking operator *numbers explodes the 3-element list into three separate positional arguments: multiply(2, 3, 4), which computes $2 \times 3 \times 4 = 24$.


5. When merging two dictionaries with {dict_a, dict_b}, what happens if both dictionaries contain the key 'port'?

A. Both values are combined into a list B. Python raises a DuplicateKeyError C. The value from dict_b overwrites the value from dict_a D. The key 'port' is deleted from the merged dictionary Answer: C Explanation: During dictionary unpacking, evaluation proceeds from left to right. Keys in subsequent dictionaries overwrite identical keys from preceding dictionaries (last-write-wins).


Practice Challenge

Scenario: Extensible Indian Microservices API Gateway Router

Microservice API gateways inspect and route incoming HTTP requests to internal microservices (Auth, Payment, Order, Notification).

Build an extensible API Router function route_request(endpoint, *path_params, **query_and_headers):

  1. 1
    Validates that endpoint is one of ("auth", "pay", "order", "notify").
  2. 2
    Gathers any variable URL route parameters (e.g. "user", 1042) into a URL path: /api/v1/<endpoint>/param1/param2/....
  3. 3
    Separates headers (keys starting with "header_") from query parameters into two separate dictionaries.
  4. 4
    Returns a structured routing summary dictionary.

Starter Code

Python
def route_request(endpoint, *path_params, **query_and_headers):
# TODO: Validate endpoint, construct path string, separate headers and query params
pass

Complete Solution

Python
ALLOWED_ENDPOINTS = ("auth", "pay", "order", "notify")
 
def route_request(endpoint, *path_params, **query_and_headers):
clean_endpoint = endpoint.strip().lower()
if clean_endpoint not in ALLOWED_ENDPOINTS:
raise ValueError(f"Unknown endpoint '{endpoint}'. Allowed: {ALLOWED_ENDPOINTS}")
 
# Construct clean REST URL path
subpaths = "/".join(str(p) for p in path_params)
full_path = f"/api/v1/{clean_endpoint}" + (f"/{subpaths}" if subpaths else "")
 
# Segregate headers (keys prefixed with 'header_') from query params
headers = {
k.replace("header_", "").replace("_", "-").title(): v
for k, v in query_and_headers.items()
if k.startswith("header_")
}
query_params = {
k: v
for k, v in query_and_headers.items()
if not k.startswith("header_")
}
 
return {
"url": full_path,
"path_params": path_params,
"headers": headers,
"query_params": query_params
}
 
# Test the router with diverse positional and keyword arguments
route_info = route_request(
"order",
"customer", 9021, "invoice", 450,
header_authorization="Bearer JWT_TOKEN_XYZ",
header_client_id="MSK_GATEWAY_01",
currency="INR",
include_tax=True
)
 
print("=== Gateway Router Output ===")
print("Target URL :", route_info["url"])
print("Path Params :", route_info["path_params"])
print("HTTP Headers :", route_info["headers"])
print("Query Params :", route_info["query_params"])

Expected Output

Output
=== Gateway Router Output ===
Target URL : /api/v1/order/customer/9021/invoice/450
Path Params : ('customer', 9021, 'invoice', 450)
HTTP Headers : {'Authorization': 'Bearer JWT_TOKEN_XYZ', 'Client-Id': 'MSK_GATEWAY_01'}
Query Params : {'currency': 'INR', 'include_tax': True}
Next Lesson

Default vs Keyword Arguments

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext