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.

Code braces with terminal and markup symbols representing Python programming.
Inside this lesson 10 sections 5 questions 8 figures

SECTION 1

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.

SECTION 2

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.

SECTION 3

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.

How *args and **kwargs Pack ArgumentsFunction Callgreet("Hi", "Alice", "Bob", age=16, grade="11th")"Hi" → regular param msg"Alice" → extra positional"Bob" → extra positionalage=16 → extra keywordgrade="11th" → extra keyworddef greet(msg, *args, **kwargs):Inside the Functionmsg = "Hi"args (tuple)("Alice", "Bob")kwargs (dict){"age": 16, "grade": "11th"}✓ All arguments accounted for!Nothing is lost, nothing is wasted.
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.

SECTION 4

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

deffunc(a,b,∗args):→args=tuple(allpositional[2:])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

deffunc(a,∗∗kwargs):→kwargs=name:valueforeachextrakeyworddef 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)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.

SECTION 5

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.

Required Parameter Order in Python Function Signaturesdef func( regular, *args, keyword_only, **kwargs ):① RegularPositional or keyworddef f(a, b=10):a → requiredb → default value→② *argsExtra positional → tupledef f(a, *args):f(1, 2, 3)args = (2, 3)→③ Keyword-onlyAfter *args; must use namedef f(*args, sep):f(1, 2, sep="-")sep MUST be keyword→④ **kwargsExtra keywords → dictdef f(**kwargs):f(x=1, y=2)kwargs = {"x":1,"y":2}Full Signature Exampledef create_profile(name, *hobbies, verified=False, **details):① name (regular)② *hobbies (tuple)③ verified (kw-only)④ **details (dict)create_profile("Alice", "chess", "coding", verified=True, age=16, school="Lincoln HS")
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.
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}

Summary of all parameter types and their resulting data types.

SECTION 6

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. 1

    Step 1 — Define the Function Signature

    We 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. 2

    Step 2 — Process the *messages Tuple

    Inside 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. 3

    Step 3 — Process the **metadata Dictionary

    The 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. 4

    Step 4 — Assemble and Print the Log Entry

    We combine the level, the joined message, and the details string into a formatted output.
    print(f"[{level}] {full_msg} | {details}")
  5. 5

    Step 5 — Call the Function

    Now 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.

SECTION 7

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.

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)

Side-by-side comparison of *args and **kwargs.

🎯

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.

SECTION 8

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.

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.

How *args and **kwargs connect to more advanced Python patterns.

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.

SECTION 9

Practice Problems

Question 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?
Show answer
*args collects extra positional arguments (those passed without a keyword name) and stores them in a tuple. **kwargs collects extra keyword arguments (those passed with a name like color="red") and stores them in a dictionary. The asterisks do the work — the names args and kwargs are just conventions. You could use any valid variable name.

Question 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.
Show answer
Here is the solution: def multiply_all(*args): result = 1 for num in args: result *= num return result We initialize result to 1 (the multiplicative identity) instead of 0, because multiplying by 0 would always give 0. Then we loop through each number in the args tuple and multiply it into the running total. If no arguments are passed, the function returns 1.

Question 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"}.
Show answer
def build_profile(name, *hobbies, **info): profile = {"name": name, "hobbies": list(hobbies)} profile.update(info) return profile The function creates a dictionary with name and converts the hobbies tuple into a list (since we want a mutable list in the output). Then profile.update(info) merges all the keyword arguments from **info into the profile dictionary. This uses all three parameter types — regular, *args, and **kwargs — in one function.

Question 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).
Show answer
def make_tag(tag, *content, **attributes): attr_str = "" for k, v in attributes.items(): attr_str += f' {k.rstrip("_")}="{v}"' body = " ".join(content) return f"<{tag}{attr_str}>{body}</{tag}>" This function demonstrates a realistic use case. The *content parameter gathers any number of text nodes, while **attributes captures HTML attributes. The rstrip("_") trick handles Python reserved words like class by allowing the caller to write class_ instead.

Question 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.
Show answer
The values are: a = 1, b = 2, args = (3, 4), mode = "multiply", kwargs = {"x": 10, "y": 20}. The key insight is that mode is a named parameter in the function signature, so it is matched directly. Only keyword arguments that do NOT match any named parameter go into **kwargs. Since mode appears after *args, it becomes a keyword-only parameter, which means it must be passed by name but is not part of kwargs. Python processes named parameters first, and only the leftovers go into kwargs.

SUMMARY

Lesson Summary

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)