1. Home
  2. Blog
  3. Software Development

Software Development

Python *args and **kwargs Explained: Flexible Functions With Clear Examples

Those stars in function signatures are less mysterious than they look. Learn how *args and **kwargs collect arguments, how the same stars unpack them, and how to design function signatures that are hard to misuse.

8 min read
Python *args and **kwargs explained: flexible functions with clear examples

Key takeaways

  • *args collects extra positional arguments into a tuple.
  • **kwargs collects extra keyword arguments into a dictionary.
  • The names are only a convention, as the * and ** are what matter.
  • The same stars unpack a list or dictionary when you call a function.

Sooner or later, you will read a function like def handler(*args, **kwargs) and wonder what it accepts. Python *args and **kwargs let a function take a flexible number of arguments. They power decorators, wrappers and many library APIs.

This guide covers how they work in both directions, along with the newer ways Python lets you control how a function is called. We ran each example on Python 3.13, and the output shown comes from that run.

What *args does

A parameter written with one star collects any extra positional arguments into a tuple:

def total(*args):
    print(type(args).__name__, args)
    return sum(args)

print(total(1, 2, 3))
# tuple (1, 2, 3)
# 6

print(total())
# tuple ()
# 0

With no arguments, you simply get an empty tuple, so the function still works. The name args is a convention. *numbers would behave the same, and a more descriptive name is often better.

What **kwargs does

Two stars collect any extra keyword arguments into a dictionary:

def describe(**kwargs):
    print(type(kwargs).__name__, kwargs)

describe(name="Asha", city="Pune")
# dict {'name': 'Asha', 'city': 'Pune'}

Because it is a normal dictionary, you can loop over it, look up keys with get() and pass it on. Keys keep the order in which they were passed.

Parameter order

When you mix kinds of parameters, Python requires a specific order:

  1. Regular positional parameters
  2. *args
  3. Keyword-only parameters
  4. **kwargs
def order(item, qty=1, *extras, priority=False, **notes):
    print(item, qty, extras, priority, notes)

order("pen")
# pen 1 () False {}

order("pen", 2, "gift wrap", "card", priority=True, note="leave at door")
# pen 2 ('gift wrap', 'card') True {'note': 'leave at door'}

Notice priority. Because it comes after *extras, it can only be set by name, so a stray positional argument could never set it by accident. Putting **kwargs earlier is not allowed, and our test raised a SyntaxError: “arguments cannot follow var-keyword argument”.

Unpacking when calling

The same stars work in reverse at the call site. One star spreads a list or tuple into positional arguments, and two stars spread a dictionary into keyword arguments:

nums = [4, 5, 6]
total(*nums)            # same as total(4, 5, 6) -> 15

settings = {"name": "Ben", "city": "Delhi"}
describe(**settings)    # same as describe(name="Ben", city="Delhi")

Unpacking works with any function, not only those that use *args and **kwargs. That is handy when configuration arrives as a dictionary:

def connect(host, port, timeout=5):
    return f"{host}:{port} (timeout {timeout})"

connect(**{"host": "db.local", "port": 5432})
# 'db.local:5432 (timeout 5)'

connect(**{"host": "db.local", "port": 5432, "ssl": True})
# TypeError: connect() got an unexpected keyword argument 'ssl'

That error is useful. A function without **kwargs rejects unknown options, so a typo in a setting name fails loudly instead of being silently ignored.

Keyword-only arguments

A bare * in the signature, with no name, means “everything after this must be passed by keyword”:

def make_user(name, *, admin=False):
    return name, admin

make_user("Asha", admin=True)
# ('Asha', True)

make_user("Asha", True)
# TypeError: make_user() takes 1 positional argument but 2 were given

This is a great way to protect boolean flags. A call like make_user("Asha", True) forces readers to guess what True means. Requiring admin=True makes every call self-explanatory.

Positional-only arguments

Since Python 3.8, a / marks parameters that can only be passed by position:

def area(width, height, /):
    return width * height

area(3, 4)                 # 12
area(width=3, height=4)
# TypeError: area() got some positional-only arguments
#   passed as keyword arguments: 'width, height'

This lets a library rename those parameters later without breaking anyone’s code, since nobody can depend on the names. Several built-ins work this way. For example, len(obj=[1]) does not work.

Tip: The full pattern is def f(pos_only, /, normal, *, kw_only). You rarely need all three, but knowing what / and * mean helps you read documentation and library signatures.

Decorators and wrappers

This is the main real-world reason to use both together. A wrapper often needs to accept whatever arguments the original function takes, and pass them on unchanged:

import functools, time

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.perf_counter() - start:.4f}s")
        return result
    return wrapper

@timed
def slow_add(a, b, delay=0.01):
    time.sleep(delay)
    return a + b

print(slow_add(2, 3, delay=0.02))
# slow_add took 0.0205s
# 5

The wrapper does not need to know slow_add‘s parameters at all, so it works for any function. Your timing figure will differ slightly. Our guide to Python decorators explains functools.wraps and other details.

Merging options

** also works inside a dictionary literal, which gives a neat way to apply overrides on top of defaults:

defaults = {"theme": "light", "lang": "en"}

def build(**overrides):
    return {**defaults, **overrides}

build(theme="dark")
# {'theme': 'dark', 'lang': 'en'}

Later keys win, so the override replaces the default. Subclasses also use this pattern, as in super().__init__(*args, **kwargs), to pass extra arguments through to a parent class. See Python classes and objects for more on super().

Cheat sheet

Syntax Where Meaning
def f(*args) Definition Collect extra positional arguments into a tuple
def f(**kwargs) Definition Collect extra keyword arguments into a dict
def f(a, *, b) Definition b is keyword-only
def f(a, /, b) Definition a is positional-only
f(*items) Call Spread a sequence into positional arguments
f(**options) Call Spread a dict into keyword arguments
{**a, **b} Literal Merge dictionaries
[*a, *b] Literal Combine iterables into a list

When to avoid them

Flexibility has a price. A function defined as def create(**kwargs) tells readers nothing about which options exist, and editors cannot autocomplete them. Typos may pass silently through several layers before anything fails.

So prefer explicit parameters when you know what a function needs. Use *args and **kwargs when you genuinely need flexibility: wrappers, decorators, forwarding arguments to another function, or a true variable-length input like total(*numbers). If a function needs many named options, a dataclass can also be clearer.

Common mistakes

Using a mutable default

This is not about stars, but it often appears near them. A default value is created once, when the function is defined, and then shared between calls:

def bad(items=[]):
    items.append(1)
    return items

print(bad(), bad())
# [1, 1] [1, 1]

Both calls returned the very same list, which by then held two items. Use items=None and create a new list inside the function.

Other errors

  • Forgetting to unpack. Calling total(nums) passes one list rather than three numbers. Write total(*nums).
  • Swallowing typos. A function that accepts **kwargs but ignores unknown keys hides misspelled options. Validate them, or avoid **kwargs.
  • Passing the same argument twice. f(1, **{"a": 2}), where a is the first parameter, raises a “multiple values” error.
  • Not forwarding in a wrapper. Calling func(args, kwargs) passes a tuple and a dict. The correct form is func(*args, **kwargs).

Type hints for *args and **kwargs

Type hints on *args and **kwargs describe each individual value, not the whole collection. This surprises many people:

def total(*prices: float) -> float:
    return sum(prices)      # prices is tuple[float, ...]

def tag(**attrs: str) -> str:
    return " ".join(f'{k}="{v}"' for k, v in attrs.items())   # dict[str, str]

total(1.5, 2.5)                  # 4.0
tag(id="x", role="button")       # id="x" role="button"

Here prices is treated as a tuple of floats and attrs as a dictionary of strings. When keyword arguments have different types, Python 3.12 added a precise option: a TypedDict combined with Unpack.

from typing import TypedDict, Unpack

class Options(TypedDict, total=False):
    timeout: float
    retries: int

def fetch(url: str, **opts: Unpack[Options]) -> None: ...

A type checker such as mypy or Pyright can then reject fetch(url, retries="3") or a misspelt option name, which a plain **kwargs can never do. Runtime behaviour does not change, though. When we ran fetch("u", retries="3") under Python 3.13, it executed without complaint, because Python itself does not enforce type hints.

If you find yourself documenting a long list of accepted keys for **kwargs, consider replacing them with explicit keyword-only parameters. Editors can then autocomplete them, and mistakes are caught immediately.

Final thoughts

Remember the two directions. In a definition, stars collect. In a call, they unpack. Add keyword-only arguments to make calls clearer, and reserve **kwargs for code that genuinely forwards arguments. To practise, write a small decorator that logs every call’s arguments, and use it on a few different functions.

Frequently asked questions

What is the difference between *args and **kwargs?

*args collects extra positional arguments into a tuple. **kwargs collects extra keyword arguments into a dictionary. A function can use both together.

Do I have to call them args and kwargs?

No. The stars do the work, so *values or **options are equally valid. args and kwargs are simply a widely recognised convention.

What order should *args and **kwargs go in?

Regular parameters first, then *args, then keyword-only parameters, and **kwargs last. Any other order causes a SyntaxError.

What does a single * mean in a function definition?

A bare * means every parameter after it must be passed by keyword. It makes calls clearer, especially for boolean flags.

What does / mean in a Python function?

Parameters before / are positional-only, so callers cannot pass them by name. This feature was added in Python 3.8.

Keep learning

Work with Stack Quora

Need help with testing, WordPress or SEO?

We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.

Keep reading

More guides on testing, AI & modern development

Enjoyed this article? Pick a topic and jump straight into our newest tutorials.

Artificial Intelligence

LLM Structured Output Explained: Getting Reliable JSON From AI Models

LLM structured output explained with validation we ran in Pydantic: JSON mode and schemas, constrained decoding, the ways model output…

Read article →
DevOps

SSH Keys Explained: How They Work, ssh-keygen, Ed25519 and Safe Habits

SSH keys explained with keys we generated: public vs private keys, ssh-keygen, Ed25519 vs RSA, fingerprints, passphrases, ssh-agent, authorized_keys, config…

Read article →
DevOps

HTTP Caching Explained: Cache-Control, ETag and 304 Responses, With a Live Test

HTTP caching explained with a small server we ran: Cache-Control directives, max-age, no-cache vs no-store, ETag revalidation and 304 responses,…

Read article →
Test Automation

API Contract Testing Explained: Catch Breaking Changes Before They Ship

API contract testing explained with a schema check we ran: what a contract is, consumer-driven contracts, Pact, OpenAPI, which changes…

Read article →
Software Testing

Risk-Based Testing Explained: Score Risks, Build a Matrix and Test What Matters

Risk-based testing explained step by step: identifying risks, scoring likelihood and impact, building a risk matrix, mapping risk to test…

Read article →
Databases & SQL

SQL Constraints Explained: NOT NULL, UNIQUE, CHECK and FOREIGN KEY, Tested

SQL constraints explained with tests we ran: NOT NULL, UNIQUE, CHECK, DEFAULT, PRIMARY and FOREIGN KEY, the NULL loopholes, SQLite's…

Read article →
Databases & SQL

SQL Date Functions Explained: Adding, Comparing and Grouping Dates Without Bugs

SQL date functions explained with queries we ran: adding intervals, month-end surprises, date differences, the BETWEEN trap, grouping by day,…

Read article →
Software Development

Python asyncio Explained: async, await, gather and TaskGroup With Real Timings

Python asyncio explained with code we timed: coroutines, await, gather, TaskGroup, error handling, timeouts, limiting concurrency and the blocking-call mistake…

Read article →
Software Development

Debounce vs Throttle in JavaScript: How They Work, When to Use Each, Tested

Debounce vs throttle in JavaScript explained with timings we measured in Node.js: how each works, simple implementations, leading and trailing…

Read article →
Software Development

Python Context Managers Explained: The with Statement, contextlib and Real Uses

Python context managers explained with code we ran: how with works, __enter__ and __exit__, contextlib.contextmanager, suppress, ExitStack, database transactions and…

Read article →
Artificial Intelligence

LLM Temperature and Top-p Explained: How AI Models Choose Their Next Word

LLM temperature and top-p explained with a simulation we ran: how probabilities are reshaped, what top-p keeps, recommended settings by…

Read article →
DevOps

YAML Explained: Syntax, the Norway Problem and Other Traps We Tested

YAML explained with a parser test: basic syntax, lists and maps, the Norway problem, versions turning into floats, octal zip…

Read article →
DevOps

Cron Jobs Explained: Syntax, Real Schedules and the Mistakes That Break Them

Cron jobs explained with schedules we verified: the five fields, operators, real examples, the day-of-month OR day-of-week trap, environment problems,…

Read article →
Software Testing

Bug Life Cycle in Software Testing: Every Status, Who Owns It and Common Mistakes

The bug life cycle explained: every defect status from New to Closed, who owns each step, rejected, deferred and reopened…

Read article →
Test Automation

Property-Based Testing in Python: Hypothesis Finds the Bugs Your Examples Miss

Property-based testing explained with Hypothesis tests we ran: properties vs examples, strategies, shrinking, the real bugs it found in our…

Read article →
Databases & SQL

SQL Pagination Explained: OFFSET vs Keyset, With a Million-Row Test

SQL pagination explained with a million-row test we ran: LIMIT and OFFSET, why deep pages get slow, keyset (cursor) pagination,…

Read article →
Databases & SQL

SQL EXISTS vs IN: Differences, the NOT IN NULL Trap and Performance

SQL EXISTS vs IN explained with queries we ran: how each works, why NOT IN returns no rows when a…

Read article →
Software Development

Python Logging Explained: Levels, Handlers, Formatters and Mistakes to Avoid

Python logging explained with code we ran: levels, basicConfig quirks, named loggers, handlers and formatters, logger.exception, lazy formatting, duplicate logs…

Read article →
Software Development

JavaScript Map vs Object: Key Differences, Pitfalls and a Benchmark

JavaScript Map vs Object explained with code we ran in Node.js: key types, ordering, the prototype trap, JSON, size, WeakMap…

Read article →
Software Development

Python Type Hints Explained: Annotations, mypy and the Errors It Catches

Python type hints explained with a real mypy run: basic annotations, Optional and unions, TypedDict, Literal, Protocol, generics, Callable, strict…

Read article →
Artificial Intelligence

LLM Tokens Explained: How AI Models Read Text, Count Costs and Fill Context Windows

LLM tokens explained with a tokenizer we ran: how text is split, why counts differ by language and tokenizer, context…

Read article →
DevOps

Semantic Versioning Explained: MAJOR.MINOR.PATCH, Ranges and Release Rules

Semantic versioning explained with code we ran: MAJOR.MINOR.PATCH rules, pre-releases, correct version sorting, caret and tilde ranges, 0.x versions, lockfiles…

Read article →
DevOps

Git Reset vs Revert: How to Undo Commits Safely, With a Tested Example

Git reset vs revert explained with a repository we built: soft, mixed and hard reset, revert commits, reverting merges, recovering…

Read article →
Software Testing

The Test Pyramid Explained: Layers, Ratios and When to Break the Shape

The test pyramid explained: unit, integration and end-to-end layers, sensible ratios, the ice cream cone anti-pattern, the testing trophy and…

Read article →
Test Automation

pytest Fixtures Explained: Scope, yield Teardown, conftest.py and Factories

pytest fixtures explained with a suite we ran: how fixtures work, scopes, yield teardown, conftest.py, factory fixtures, parametrized fixtures, built-ins…

Read article →
Databases & SQL

SQL Upsert Explained: INSERT ON CONFLICT, MERGE and the REPLACE Trap

SQL upsert explained with queries we ran: INSERT ON CONFLICT DO UPDATE and DO NOTHING, the excluded row, conditional updates,…

Read article →
Databases & SQL

SQL LIKE Operator Explained: Wildcards, Case Sensitivity and Performance

The SQL LIKE operator explained with queries we ran: % and _ wildcards, escaping, NOT LIKE and NULL, case sensitivity…

Read article →
Artificial Intelligence

AI Hallucinations Explained: Why Language Models Make Things Up and How to Reduce It

AI hallucinations explained: what they are, why language models produce them, common types, how to reduce them with grounding and…

Read article →
Test Automation

Mocking in Python: unittest.mock, patch and the Mistakes That Make Tests Lie

Mocking in Python with unittest.mock, tested: Mock and return_value, side_effect, patching where a name is used, spec and autospec, mocking…

Read article →
Software Testing

Code Coverage Explained: Line vs Branch Coverage and What 100% Really Means

Code coverage explained with a real coverage.py run: line, branch and condition coverage, reading a report, why 100% coverage can…

Read article →
Artificial Intelligence

What Are AI Agents? How They Work, With a Simple Agent Loop You Can Run

What are AI agents? A plain guide to the agent loop, tools, memory and guardrails, with a small Python loop…

Read article →
Test Automation

TDD vs BDD: Key Differences, One Feature Built Both Ways and When to Use Each

TDD vs BDD explained with one discount feature built both ways: red-green-refactor with code we ran, Given-When-Then scenarios, tools, trade-offs…

Read article →
Software Testing

User Acceptance Testing (UAT): Process, Checklist and Real Examples

User acceptance testing explained: who runs it, how it differs from QA, a seven-step process, a UAT test case template,…

Read article →
Artificial Intelligence

What Are Embeddings? Vectors and Cosine Similarity Explained Simply

What are embeddings? A plain-language guide to vectors, cosine similarity and semantic search, with a small example we ran, common…

Read article →
Test Automation

Flaky Tests: What Causes Them and How to Fix Them for Good

Flaky tests explained with demos we ran: the main causes, how to confirm and diagnose one, proper fixes for timing,…

Read article →
Software Testing

Load Testing vs Stress Testing: Differences, Metrics and k6 Examples

Load testing vs stress testing explained: what each proves, spike and soak tests, the metrics that matter, why averages mislead,…

Read article →