PYTHON • FUNCTIONS AND PROGRAM STRUCTURE

*args & **kwargs — Use *args and **kwargs (intro)

Write flexible Python functions that accept any number of arguments without rewriting your code.

Historical Context & Motivation

Imagine you are building a function that adds numbers together. You might start by writing a function that takes two numbers, but what happens when you need to add three, five, or even twenty numbers? You would have to write a brand-new function every time, or keep adding more and more parameters. That gets messy fast. Python's creators recognized this problem early on and built a powerful feature directly into the language: variable-length arguments, commonly known as *args and **kwargs.

The idea of passing a variable number of arguments to a function did not originate with Python. Older languages like C had similar mechanisms (the ... ellipsis notation), but they were error-prone and hard to use. Python's approach, refined over decades of development, made variable arguments both safe and elegant. Let's trace a few key moments in this evolution.

1972
C Language Variadic Functions
The C programming language introduces ... for variable argument lists, but with no type safety and a clunky macro-based interface.
1991
Python 0.9 Released
Guido van Rossum releases the first version of Python, which already supports *args for positional variable arguments, making flexible functions far easier to write.
1994
Python 1.0 & **kwargs
Python 1.0 solidifies support for **kwargs, enabling functions to accept any number of keyword arguments as a dictionary.
2008
Python 3.0 Refines Syntax
Python 3 cleans up function signatures and introduces keyword-only arguments using a bare * separator, building on the same packing mechanism.

The core question this lesson addresses is simple but important: How do you write a single function that can handle any number of inputs, without knowing in advance how many there will be? Python's answer — *args and **kwargs — is one of the most useful tools in any Python programmer's toolkit.

Core Principles & Definitions

Before diving into code, let's nail down the key ideas. The single asterisk * and double asterisk ** in front of a parameter name are not just decoration — they tell Python to pack extra arguments into a single container. The names args and kwargs are just conventions — you could call them *stuff and **options if you wanted. It's the asterisks that do the magic.

1

*args — Positional Packing

The *args parameter collects all extra positional arguments into a tuple. You can then loop through them or index into them just like any tuple.
2

**kwargs — Keyword Packing

The **kwargs parameter collects all extra keyword arguments into a dictionary. Each key is the argument name, and each value is the argument's value.
3

Parameter Order Matters

In a function signature, the order must be: regular params → *args → keyword-only params → **kwargs. Breaking this order causes a SyntaxError.
4

Unpacking: The Reverse

You can also use * and ** when calling a function to unpack a list or dictionary into separate arguments.
✦ KEY TAKEAWAY
Think of *args like a backpack: you can toss in as many items (arguments) as you want, and Python zips them up into one neat bundle (a tuple). **kwargs is like a labeled filing cabinet: each item goes in with a name tag (keyword), and Python organizes them into a dictionary you can look up by name.

Visual Explanation

The diagram below shows exactly what happens when Python encounters a function call with extra arguments. On the left you see the arguments being passed in; on the right you see how Python packs them into the args tuple and kwargs dictionary inside the function body.

The cyan box shows the regular parameter msg being assigned directly. The violet box shows extra positional arguments packed into the args tuple. The pink box shows extra keyword arguments packed into the kwargs dictionary.

Notice that the regular parameter msg consumes the first positional argument, and only the leftover positional arguments end up in args. Similarly, only the leftover keyword arguments — those that do not match any named parameter — end up in kwargs. This is why order matters in the function definition.

How It Works Under the Hood

Packing: From Arguments to Containers

When Python sees a * in a function definition, it enters a special "packing mode." After all regular positional parameters have been satisfied, any remaining positional arguments are gathered into a tuple. A tuple is an immutable (unchangeable) sequence — think of it as a read-only list. Python chose a tuple instead of a list to signal that these packed values should not be modified inside the function.

PACKING RULE FOR *args
def func(a, b, *args): → args = tuple(all_positional[2:])
Parameters a and b consume positions 0 and 1. Everything from position 2 onward is packed into args.

Similarly, the ** operator collects leftover keyword arguments into a dictionary. A dictionary maps keys (the keyword names, which are strings) to values. This lets you access the extra arguments by name instead of by position.

PACKING RULE FOR **kwargs
def func(a, **kwargs): → kwargs = {name: value for each extra keyword}
Any keyword argument that doesn't match the named parameter a gets added to the kwargs dictionary.

Unpacking: From Containers to Arguments

The same * and ** operators can work in reverse when you call a function. If you have a list nums = [1, 2, 3] and you call add(*nums), Python unpacks the list so the function receives add(1, 2, 3). For dictionaries, **my_dict unpacks key-value pairs into keyword arguments.

UNPACKING CALL SYNTAX
func(*[1, 2, 3]) → func(1, 2, 3) | func(**{"x": 5}) → func(x=5)
The * "spreads" a list or tuple, while ** "spreads" a dictionary into keyword arguments.

Parameter Order & Classification

Python enforces a strict order for parameters in a function definition. Getting this order wrong causes a SyntaxError. The diagram below illustrates the required order from left to right, along with the data type each parameter produces inside the function.

Parameters flow left to right: regular params (blue), then *args (violet), then keyword-only params (amber), and finally **kwargs (pink). The bottom shows a realistic example with all four types.
Summary of all parameter types and their resulting data types.
Parameter TypeSymbolData Type Inside FunctionExample Values
Regular positionalaWhatever type is passeda = 5
Default valueb=10Whatever type is passed (or default)b = 10
*args*argstuple(2, 3, 4)
Keyword-onlysep="-"Whatever type is passedsep = "-"
**kwargs**kwargsdict{"x": 1, "y": 2}

Worked Example: Building a Flexible Logger

Let's build a practical logging function that accepts a log level, any number of messages, and optional metadata like a timestamp or user ID. This is the kind of function you would actually use in a real project.

Building a Flexible Logger with *args and **kwargs
1
Step 1 — Define the Function SignatureWe start by defining a function with one regular parameter level (like "INFO" or "ERROR"), then *messages for any number of log messages, and **metadata for optional extra info.
def log(level, *messages, **metadata):
2
Step 2 — Process the *messages TupleInside the function, messages is a tuple. We loop through it and join all the messages into one string separated by spaces.
full_msg = " ".join(messages)
3
Step 3 — Process the **metadata DictionaryThe metadata dictionary might contain keys like user or timestamp. We loop through the key-value pairs to build a detail string.
details = ", ".join(f"{k}={v}" for k, v in metadata.items())
4
Step 4 — Assemble and Print the Log EntryWe combine the level, the joined message, and the details string into a formatted output.
print(f"[{level}] {full_msg} | {details}")
5
Step 5 — Call the FunctionNow we call log with different numbers of arguments each time. Notice how the same function handles all these cases without any changes.
log("INFO", "Server started") → [INFO] Server started | log("ERROR", "File", "not", "found", user="alice", code=404) → [ERROR] File not found | user=alice, code=404
💡 PRO TIP
If *args receives no extra arguments, it becomes an empty tuple (). If **kwargs receives no extra keywords, it becomes an empty dictionary {}. Your code should handle these empty cases gracefully — the function won't crash, it just has nothing extra to process.

Strengths, Limitations & When to Use Each

Both *args and **kwargs make your functions more flexible, but they are not interchangeable. Each shines in different situations. Let's compare them side by side.

Side-by-side comparison of *args and **kwargs.
Feature*args**kwargs
CollectsExtra positional argumentsExtra keyword arguments
Data typetupledict
Access byIndex: args[0]Key: kwargs["name"]
Best forLists of similar items (numbers, names, paths)Configuration options with descriptive names
LimitationNo labeling — you must remember orderSlightly more verbose to call
Common use casesum_all(1, 2, 3, 4)connect(host="...", port=80)
🎯 WHEN TO USE WHICH
Use *args when your function processes a variable number of similar items — like adding numbers or joining strings. Use **kwargs when your function accepts optional settings or configuration where each argument has a meaningful name. Use both together when you want maximum flexibility — like a function that can handle any combination of inputs and options.

Connection to Advanced Python Patterns

Once you're comfortable with *args and **kwargs, you'll find them everywhere in real Python code. They are the foundation of several advanced patterns that you'll encounter as you keep learning.

How *args and **kwargs connect to more advanced Python patterns.
Intro ConceptAdvanced PatternWhy It Matters
*args in your own functionsDecorators — wrapper functions that modify other functionsDecorators use *args, **kwargs to wrap any function regardless of its signature.
**kwargs for optional settingsClass inheritance — passing constructor args up the chainChild classes use super().__init__(**kwargs) to forward arguments to parent classes.
Unpacking with *listFunctional programming — map(), zip()Functions like zip(*matrix) use unpacking to transpose data structures.
Combining *args + **kwargsAPI design — building frameworks and librariesLibraries like Flask and Django heavily use **kwargs for flexible configuration.

Don't worry about mastering these advanced patterns right now. The key takeaway is that *args and **kwargs are not just a beginner concept you'll outgrow — they are a fundamental building block that professional Python developers use every day. Learning them now prepares you for decorators, class hierarchies, and framework development later on.

Practice Problems

PROBLEM 1 — CONCEPTUAL
Explain the difference between *args and **kwargs in your own words. What data type does each produce inside the function, and what kind of arguments does each collect?
PROBLEM 2 — BASIC CALCULATION
Write a function called multiply_all that uses *args to accept any number of numbers and returns their product. For example, multiply_all(2, 3, 4) should return 24.
PROBLEM 3 — INTERMEDIATE
Write a function build_profile(name, *hobbies, **info) that returns a dictionary with the key "name" set to the name, "hobbies" set to a list of hobbies, and all the keyword arguments merged in. Calling build_profile("Maya", "art", "coding", age=15, grade="10th") should return {"name": "Maya", "hobbies": ["art", "coding"], "age": 15, "grade": "10th"}.
PROBLEM 4 — APPLIED
You are building a simple HTML tag generator. Write a function make_tag(tag, *content, **attributes) that produces HTML. For example, make_tag("p", "Hello", "World", class_="intro", id="greeting") should return the string '<p class="intro" id="greeting">Hello World</p>'. Hint: join content with a space and loop through attributes.items() to build the attribute string. Use k.rstrip("_") to strip trailing underscores from attribute names (since class is a reserved word in Python).
PROBLEM 5 — CRITICAL THINKING
Consider the function def mystery(a, b, *args, mode="add", **kwargs):. For the call mystery(1, 2, 3, 4, mode="multiply", x=10, y=20), state the exact value of each parameter inside the function: a, b, args, mode, and kwargs. Then explain why mode is NOT captured by **kwargs even though it is passed as a keyword argument.

Lesson Summary

Python's *args syntax collects extra positional arguments into a tuple, while **kwargs collects extra keyword arguments into a dictionary. The names args and kwargs are conventions — the single asterisk (*) and double asterisk (**) are what trigger the packing behavior. Together they let you write a single flexible function that handles any combination of inputs.

The required parameter order in a function signature is: regular parameters → *args → keyword-only parameters → **kwargs. You can also use * and ** when calling a function to unpack a list or dictionary into individual arguments. These tools form the foundation of advanced Python patterns including decorators, class inheritance, and API design.

Varsity Tutors • Python • *args & **kwargs — Use *args and **kwargs (intro)