Skip to content

args and *kwargs Explained

*args and **kwargs let a function accept any number of arguments. *args collects extra positional arguments into a tuple. **kwargs collects extra keyword arguments into a dictionary. Together, they make functions flexible without requiring a fixed parameter list.

Learn Your Way

Read Build Watch Test Review Visualize
You are here Projects Videos Quiz Flashcards Diagrams

Why This Matters

You will see *args and **kwargs in almost every Python library. Understanding them is essential for writing decorators, creating wrapper functions, and understanding how frameworks like Flask and pytest work under the hood. They also appear in function signatures on docs.python.org — you need to read them confidently.

*args — variable positional arguments

The * before a parameter name collects all extra positional arguments into a tuple:

def add_all(*args):
    print(type(args))    # <class 'tuple'>
    print(args)          # (1, 2, 3, 4, 5)
    return sum(args)

add_all(1, 2, 3, 4, 5)    # 15
add_all(10, 20)             # 30
add_all()                   # 0

The name args is a convention — you can use any name. The * is what matters:

def greet(*names):
    for name in names:
        print(f"Hello, {name}!")

greet("Alice", "Bob", "Charlie")

**kwargs — variable keyword arguments

The ** before a parameter name collects all extra keyword arguments into a dictionary:

def print_info(**kwargs):
    print(type(kwargs))    # <class 'dict'>
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_info(name="Alice", age=30, city="Portland")
# name: Alice
# age: 30
# city: Portland

Again, kwargs is just a convention. The ** is what matters.

Combining regular parameters, *args, and **kwargs

def make_profile(name, *hobbies, **details):
    print(f"Name: {name}")
    print(f"Hobbies: {hobbies}")
    print(f"Details: {details}")

make_profile("Alice", "reading", "hiking", age=30, city="Portland")
# Name: Alice
# Hobbies: ('reading', 'hiking')
# Details: {'age': 30, 'city': 'Portland'}

The order must be: regular parameters, then *args, then **kwargs.

Unpacking with * and **

The * and ** operators also work in the opposite direction — unpacking a sequence or dictionary into function arguments:

def add(a, b, c):
    return a + b + c

# Unpack a list into positional arguments:
numbers = [1, 2, 3]
add(*numbers)    # Same as add(1, 2, 3) → 6

# Unpack a dict into keyword arguments:
params = {"a": 10, "b": 20, "c": 30}
add(**params)    # Same as add(a=10, b=20, c=30) → 60

This is extremely useful for forwarding arguments:

def wrapper(*args, **kwargs):
    print("Before the call")
    result = original_function(*args, **kwargs)
    print("After the call")
    return result

Positional-only parameters (/)

Python 3.8+ lets you mark parameters as positional-only using /:

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

greet("Alice")                    # OK: "Hello, Alice!"
greet("Alice", greeting="Hi")    # OK: "Hi, Alice!"
greet(name="Alice")              # TypeError! name is positional-only

Everything before / must be passed by position, not by name. You see this in built-in functions like len() — you cannot write len(obj=[1,2,3]).

Keyword-only parameters (*)

A bare * in the parameter list forces everything after it to be keyword-only:

def connect(host, port, *, timeout=30, retries=3):
    print(f"Connecting to {host}:{port} (timeout={timeout})")

connect("localhost", 8080)                        # OK
connect("localhost", 8080, timeout=10)             # OK
connect("localhost", 8080, 10)                     # TypeError! timeout is keyword-only

This prevents mistakes where someone passes arguments in the wrong order.

The full parameter order

def example(pos_only, /, normal, *, kw_only, **kwargs):
    pass

# pos_only  — must be positional (before /)
# normal    — can be positional or keyword
# kw_only   — must be keyword (after *)
# **kwargs  — catches extra keyword arguments

The complete order is: 1. Positional-only parameters (before /) 2. Regular parameters 3. *args 4. Keyword-only parameters (after * or *args) 5. **kwargs

Real-world examples

Decorator that passes through all arguments:

from functools import wraps

def log_call(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

Config builder:

def create_config(name, **overrides):
    defaults = {
        "debug": False,
        "port": 8080,
        "host": "localhost",
    }
    return {**defaults, **overrides, "name": name}

config = create_config("myapp", debug=True, port=3000)
# {"debug": True, "port": 3000, "host": "localhost", "name": "myapp"}

Merging dictionaries with **:

defaults = {"color": "blue", "size": "medium"}
user_prefs = {"color": "red", "font": "Arial"}

merged = {**defaults, **user_prefs}
# {"color": "red", "size": "medium", "font": "Arial"}
# Later values override earlier ones

Common Mistakes

Mutable default arguments (not specific to *args but related):

# WRONG — the list is shared between all calls:
def add_item(item, items=[]):
    items.append(item)
    return items

add_item("a")    # ["a"]
add_item("b")    # ["a", "b"] — surprise!

# RIGHT — use None as default:
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

Forgetting to unpack:

def add(a, b):
    return a + b

args = (1, 2)
add(args)      # TypeError — passes the tuple as a single argument
add(*args)     # 3 — unpacks into two arguments

Wrong order of parameters:

# WRONG:
def bad(*args, name, **kwargs):    # name after *args is keyword-only
    pass

bad("a", "b", "Alice")    # TypeError! name must be keyword
bad("a", "b", name="Alice")    # OK

Practice

Quick check: Take the quiz (coming soon)

Review: Flashcard decks Practice reps: Coding challenges

Further Reading


← Prev Home Next →