*args & **kwargs

Reviewed & published by Brayan K

How does print() accept one argument or ten? How does a function pass along whatever it's given to another function? The answer is *args and **kwargs — Python's way of writing functions that accept a flexible, unknown number of inputs.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Once you understand the star operators, a lot of "magic" library code suddenly reads clearly.

What You'll Learn in This Lesson

1 The Problem *args Solves

Imagine writing an add function. With fixed parameters you'd have to guess how many numbers the caller might pass:

# Rigid — only ever works for exactly two numbers
def add(a, b):
    return a + b

print(add(2, 3))     # 5
# print(add(2, 3, 4))  # TypeError: too many arguments!

*args fixes this. The star tells Python: "gather all the leftover positional arguments into a single tuple called args":

def add(*args):
    print("Received:", args)   # args is a tuple
    return sum(args)

print(add(2, 3))        # Received: (2, 3)        -> 5
print(add(2, 3, 4, 5))  # Received: (2, 3, 4, 5)  -> 14
print(add())            # Received: ()            -> 0

Inside the function, args is just a normal tuple — you can loop over it, index it, or pass it to sum().

🏷️ **kwargs — Flexible Named Arguments

While *args catches extra positional arguments, **kwargs catches extra keyword (named) arguments and stores them in a dictionary:

def make_profile(**kwargs):
    print("Got dict:", kwargs)
    for key, value in kwargs.items():
        print(f"  {key}: {value}")

make_profile(name="Ada", role="engineer", city="London")

# Output:
# Got dict: {'name': 'Ada', 'role': 'engineer', 'city': 'London'}
#   name: Ada
#   role: engineer
#   city: London

Each key=value the caller supplies becomes one entry in the kwargs dictionary. This is exactly how flexible configuration functions and many web framework APIs work under the hood.

2 The Required Parameter Order

You can combine regular parameters, *args, and **kwargs — but only in one specific order. Get it wrong and Python raises a SyntaxError:

def func(positional, *args, keyword_only=..., **kwargs):
         ^^^^^^^^^^   ^^^^^   ^^^^^^^^^^^^      ^^^^^^^^
         1. normal    2. extra  3. named with    4. extra
            params      values   defaults          named
def report(title, *items, sep=" | ", **meta):
    print(title)
    print(sep.join(str(i) for i in items))
    for k, v in meta.items():
        print(f"{k} = {v}")

report("Sales", 100, 250, 90, sep=", ", region="EU", year=2024)

# Output:
# Sales
# 100, 250, 90
# region = EU
# year = 2024
# A flexible logging helper
def log(level, *messages, **context):
    line = f"[{level.upper()}] " + " ".join(str(m) for m in messages)
    if context:
        extras = ", ".join(f"{k}={v}" for k, v in context.items())
        line += f"  ({extras})"
    print(line)

log("info", "Server started")
log("warn", "High memory", "82% used", host="web-1", pid=4021)
log("error", "DB timeout", retries=3, db="orders")

# ✅ Expected output:
# [INFO] Server started
# [WARN] High memory 82% used  (host=web-1, pid=4021)
# [ERROR] DB timeout  (retries=3, db=orders)

3 The Other Direction: Unpacking at the Call Site

The star operators work in reverse too. When you call a function, * spreads a list/tuple into positional arguments and ** spreads a dict into keyword arguments:

def greet(greeting, name):
    return f"{greeting}, {name}!"

# Unpack a list into positional arguments
parts = ["Hello", "Ada"]
print(greet(*parts))            # Hello, Ada!

# Unpack a dict into keyword arguments
data = {"greeting": "Hi", "name": "Ben"}
print(greet(**data))            # Hi, Ben!

# Merge collections cleanly with unpacking
a = [1, 2]
b = [3, 4]
print([*a, *b])                 # [1, 2, 3, 4]

d1 = {"x": 1}
d2 = {"y": 2}
print({**d1, **d2})             # {'x': 1, 'y': 2}

🔁 The Classic Use: Forwarding Arguments

The most common professional reason to use both together is writing a wrapper that accepts anything and passes it straight through to another function — the heart of how decorators work:

def timed(func, *args, **kwargs):
    print(f"Calling {func.__name__}...")
    result = func(*args, **kwargs)   # forward EVERYTHING
    print("Done.")
    return result

def area(length, width, unit="m"):
    return f"{length * width} {unit}²"

# timed forwards both positional and keyword args untouched
print(timed(area, 4, 5, unit="cm"))

# Output:
# Calling area...
# Done.
# 20 cm²

Because timed doesn't care what arguments area needs, it can wrap any function at all. This "accept anything, forward everything" pattern is everywhere in real Python libraries.

🧩 Reorder Challenge

These lines should define an averaging function and call it by unpacking a list. Reorder them so the output is Average: 20.0.

A)   return sum(nums) / len(nums)
B)   def average(*nums):
C)   values = [10, 20, 30]
D)   print(f"Average: {average(*values)}")

B → A → C → D

def average(*nums):                 # B
    return sum(nums) / len(nums)     # A (indented body)
values = [10, 20, 30]               # C
print(f"Average: {average(*values)}")  # D -> Average: 20.0

Why: the def header (B) must come before its indented body (A). The values list (C) is created next, then the call (D) unpacks it with *values so the three numbers arrive as separate positional args. Their average is 20.0.

🧠 Quick Recall

1. What is the type of args here?

def f(*args):
    print(type(args))
f(1, 2, 3)

<class 'tuple'> — *args always collects extra positional arguments into a tuple, even though you call with a star.

def f(a, b):
    return a - b
print(f(*[10, 3]))

7 — *[10, 3] spreads the list so a=10 and b=3, giving 10 - 3.

3. What is the result?

print({**{"a": 1}, **{"a": 9, "b": 2}})

{'a': 9, 'b': 2} — merging dicts with ** lets later keys overwrite earlier ones, so a becomes 9.

📋 Star Operator Quick Reference

SyntaxWhereDoes
*argsDefinitionCollect extra positionals → tuple
**kwargsDefinitionCollect extra keywords → dict
*listCall siteSpread list → positional args
**dictCall siteSpread dict → keyword args
{**a, **b}LiteralMerge two dictionaries

🎯 Mini Challenge: Flexible Shopping Cart

Write a checkout function that totals any number of prices and accepts optional named settings.

# 🎯 YOUR TURN: total any number of items + options
def checkout(*prices, tax=0.0, currency="$"):
    subtotal = sum(prices)
    total = subtotal * (1 + tax)
    return f"{currency}{total:.2f}"

print(checkout(9.99, 4.50, 12.00))                 # $26.49
print(checkout(9.99, 4.50, 12.00, tax=0.2))        # $31.79
print(checkout(100, tax=0.1, currency="£"))        # £110.00

# Bonus: build a cart list and unpack it into the call
cart = [3.0, 7.5, 1.25]
print(checkout(*cart, tax=0.05))

# ✅ Expected output:
# $26.49
# $31.79
# £110.00
# $12.34

Lesson complete — your functions are flexible now!

You can collect extra arguments with *args and **kwargs, order parameters correctly, unpack collections at the call site, and forward arguments through wrappers. This unlocks a huge amount of real-world Python code.

Practice quiz

Inside def f(*args), what type is args?

  • list
  • dict
  • tuple
  • set

Answer: tuple. *args always collects extra positional arguments into a tuple.

Inside def g(**kwargs), what type is kwargs?

  • dict
  • tuple
  • list
  • set

Answer: dict. **kwargs collects extra keyword arguments into a dictionary.

Given def add(*args): return sum(args), what does add() return?

  • None
  • an error
  • ()
  • 0

Answer: 0. With no arguments, args is the empty tuple () and sum(()) is 0.

Given def sub(a, b): return a - b, what does sub(*[10, 3]) print?

  • 13
  • 7
  • [10, 3]
  • an error

Answer: 7. *[10, 3] spreads the list so a=10 and b=3, giving 10 - 3 = 7.

What is the result of {**{'a': 1}, **{'a': 9, 'b': 2}}?

  • {'a': 9, 'b': 2}
  • {'a': 1, 'b': 2}
  • {'a': 10, 'b': 2}
  • an error

Answer: {'a': 9, 'b': 2}. When merging dicts with **, later keys overwrite earlier ones, so a becomes 9.

Which parameter order is valid in a function signature?

  • def f(**kwargs, *args)
  • def f(*args, a, **kwargs)
  • def f(a, *args, **kwargs)
  • def f(*args, a)

Answer: def f(a, *args, **kwargs). The required order is: normal params, then *args, then keyword-only params, then **kwargs.

In a function DEFINITION, what does the * in *args do?

  • Spreads a tuple into arguments
  • Collects extra positionals into a tuple
  • Multiplies the arguments
  • Nothing

Answer: Collects extra positionals into a tuple. In a definition the star collects; at a call site it spreads. Same symbol, mirror jobs.

A parameter listed AFTER *args in a signature becomes...

  • positional-only
  • optional
  • ignored
  • keyword-only

Answer: keyword-only. Anything after *args must be passed by name, making it keyword-only.

What does [*[1, 2], *[3, 4]] evaluate to?

  • [[1, 2], [3, 4]]
  • [1, 2, 3, 4]
  • [4, 6]
  • an error

Answer: [1, 2, 3, 4]. Unpacking with * spreads both lists into one flat list [1, 2, 3, 4].

Why is *args/**kwargs ideal for writing a wrapper or decorator?

  • It runs faster
  • It hides the original function
  • It forwards any arguments straight through to the wrapped function
  • It removes default values

Answer: It forwards any arguments straight through to the wrapped function. The wrapper accepts anything and forwards it with func(*args, **kwargs), so it can wrap any function.

Continue this course

Frequently asked questions

What do *args and **kwargs actually mean?

They let a function accept a flexible number of arguments. *args collects any extra positional arguments into a tuple, and **kwargs collects any extra keyword arguments into a dictionary. The single star handles values passed by position; the double star handles values passed by name.

Are the names 'args' and 'kwargs' required?

No — only the stars matter. You could write *items and **options and it works identically. But args (arguments) and kwargs (keyword arguments) are such a strong convention that every Python developer recognizes them instantly, so stick with them unless a clearer name genuinely helps.

What's the correct order of parameters in a function signature?

Always: regular positional parameters, then *args, then keyword-only parameters, then **kwargs. For example def f(a, b, *args, sep=' ', **kwargs). Putting them out of order is a SyntaxError, because Python needs the catch-alls last to know where the named parameters end.

What does the star do when CALLING a function, not defining one?

It unpacks. At a call site, *some_list spreads a list into separate positional arguments and **some_dict spreads a dict into keyword arguments. So if nums = [1, 2, 3], then add(*nums) is the same as add(1, 2, 3). It's the mirror image of collecting.

When should I avoid *args and **kwargs?

When the arguments are actually known and fixed — explicit parameters are clearer and give better editor hints and error messages. Reach for *args/**kwargs when you genuinely don't know how many inputs you'll get, or when writing a wrapper/decorator that must forward whatever it receives.