DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
Getting Started
01
What is Python and Why Learn It in 2026
02
Installing Python on Windows, Mac, and Linux
03
Setting up VS Code / PyCharm for Python
04
Understanding the Python Interpreter and REPL
05
Writing and Running Your First Python Program
06
Python Syntax and Indentation Rules
Variables and Data Types
01
Variables and Naming Conventions in Python
02
Numbers in Python (int, float, complex)
03
Strings and String Methods in Python
04
Booleans in Python
05
Type Conversion / Casting in Python
06
Understanding the None Type in Python
Operators
01
Arithmetic Operators in Python
02
Comparison Operators in Python
03
Logical Operators in Python
04
Assignment Operators in Python
05
Bitwise Operators in Python
06
Identity (is) and Membership (in) Operators
07
Walrus Operator (:=) in Python
Input, Output & Strings
01
input() and print() Functions in Python
02
f-strings and String Formatting in Python
03
String Slicing and Indexing in Python
04
Common String Methods (split, join, strip, replace)
05
Escape Characters in Python Strings
Control Flow
01
if, elif, else Statements in Python
02
Nested Conditionals in Python
03
for Loops in Python
04
while Loops in Python
05
break, continue, and pass Statements
06
Loop with else Clause in Python
07
match-case (Structural Pattern Matching)
Data Structures
01
Python Lists: Creation, Indexing, Slicing, Methods
02
Python Tuples and Immutability
03
Python Sets and Set Operations
04
Python Dictionaries: Keys, Values, and Methods
05
List Comprehensions in Python
06
Dictionary & Set Comprehensions in Python
07
Working with Nested Lists and Dictionaries
Functions
01
Defining and Calling Functions in Python
02
Function Arguments: Positional, Keyword, Default
03
*args and **kwargs in Python
04
Return Statements in Python Functions
05
Lambda Functions in Python
06
Recursion in Python
07
Variable Scope: Local, Global, Nonlocal
08
Docstrings and Function Annotations
Functional Programming Basics
01
map(), filter(), and reduce() in Python
02
Closures in Python Explained
03
Python Decorators: Basic to Advanced
04
Generators and yield in Python
Object-Oriented Programming (OOP)
01
Classes and Objects in Python
02
The init Constructor in Python
03
Instance vs Class Variables in Python
04
Inheritance in Python (Single, Multiple, Multilevel)
05
Polymorphism in Python
06
Encapsulation in Python
07
Abstraction in Python with the abc Module
08
Magic/Dunder Methods in Python
09
Static and Class Methods in Python
10
The @property Decorator in Python
11
Python Dataclasses Explained
Error Handling
01
try, except, finally in Python
02
Handling Multiple Exceptions in Python
03
Raising Custom Exceptions in Python
04
Understanding Python's Exception Hierarchy
05
Assertions in Python
File Handling
01
Reading and Writing Text Files in Python
02
Working with the with Statement in Python
03
Working with CSV Files in Python
04
Working with JSON Files in Python
05
Working with Directories (os, pathlib) in Python
Modules and Packages
01
Importing Built-in Modules in Python
02
Creating Your Own Modules in Python
03
Understanding Packages and init.py
04
Python Standard Library Overview
05
Installing Packages with pip
Advanced Core Concepts
01
Iterators and Iterables in Python
02
Context Managers in Python (with, contextlib)
03
Multithreading Basics in Python
04
Multiprocessing Basics in Python
05
Asyncio Basics: async/await in Python
06
Memory Management and Garbage Collection in Python
07
Type Hints in Python (typing module)

*args and **kwargs in Python

The previous article touched on *args and **kwargs briefly while covering the full parameter-ordering rules — this one is dedicated entirely to python *args **kwargs, covering exactly how each works, how to unpack existing collections into function calls, and where this pattern genuinely earns its place versus where it just adds unnecessary complexity.

Introduction: The Problem *args and **kwargs Solve

The pain point without them

Imagine writing a function that adds numbers together, but you don't know in advance how many numbers the caller will want to add. Without a mechanism for variable-length arguments, you'd be stuck writing something impractical:

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

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

def add_four(a, b, c, d):
    return a + b + c + d
# ...and so on, forever

That obviously doesn't scale. *args and **kwargs exist specifically to solve this: accepting any number of arguments through a single, flexible parameter.

The quick framing

*args packs any extra positional arguments into a tuple. **kwargs packs any extra keyword arguments into a dictionary.

A naming clarification worth getting right

Here's something worth being precise about early: args and kwargs are just conventional names — Python doesn't require those exact words. What actually matters, syntactically, is the * and ** operators in front of them. You could technically write *numbers or **options instead, and it would work identically. That said, *args and **kwargs are such a strong, universal convention across the Python ecosystem that deviating from them without a good reason (like *numbers genuinely being more descriptive in a specific context) tends to make code slightly less immediately recognizable to other Python developers.

*args: Variable-Length Positional Arguments

Basic syntax and example
def sum_all(*args):
    return sum(args)

print(sum_all(1, 2))            # 3
print(sum_all(1, 2, 3, 4, 5))   # 15
print(sum_all())                 # 0 — args is just an empty tuple

Inside the function, args is a regular tuple, built automatically from however many positional arguments the caller actually supplied. You can loop over it, index into it, or pass it to sum() exactly like any other tuple.

Combining *args with regular positional parameters

You can mix explicit, named positional parameters with *args — the named ones fill first, and anything left over gets packed into args:

def describe_trip(destination, *stops):
    print(f"Destination: {destination}")
    for stop in stops:
        print(f"  Stop: {stop}")

describe_trip("Paris", "London", "Amsterdam", "Berlin")
# Destination: Paris
#   Stop: London
#   Stop: Amsterdam
#   Stop: Berlin

destination claims the first positional argument, and everything after it gets swept into stops as a tuple.

Unpacking a list or tuple into separate arguments

The * operator works in the opposite direction too — at call time, prefixing an existing list or tuple with * unpacks it into separate positional arguments:

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

numbers = [1, 2, 3]
print(add(*numbers))   # 6 — equivalent to add(1, 2, 3)

Without the *, add(numbers) would try to pass the entire list as a single argument, which would fail since add() expects three separate values. *numbers spreads the list's contents out into individual arguments instead.

The error when the count doesn't match

If the unpacked collection doesn't have exactly the right number of items for the function's positional parameters, Python raises an error immediately:

numbers = [1, 2]
print(add(*numbers))
# TypeError: add() missing 1 required positional argument: 'c'

**kwargs: Variable-Length Keyword Arguments

Basic syntax and example
def print_settings(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_settings(timeout=5, retries=3, debug=True)
# timeout: 5
# retries: 3
# debug: True

Inside the function, kwargs is a regular dictionary, built from whatever named arguments the caller happened to pass — no fixed set of expected keyword names required in the function definition itself.

Unpacking a dictionary into keyword arguments

The ** operator, like *, works in reverse at call time — prefixing an existing dictionary with ** unpacks its key-value pairs into separate keyword arguments:

def create_user(name, email, role):
    print(f"{name} ({email}) — {role}")

user_data = {"name": "Alex", "email": "[email protected]", "role": "admin"}
create_user(**user_data)
# Alex ([email protected]) — admin

This is a genuinely common pattern when working with data that's already shaped as a dictionary — say, a parsed JSON object or a database record — and you need to pass its fields into a function expecting individual keyword arguments.

Practical pattern: kwargs.get() for flexible configuration

Inside a function accepting **kwargs, .get() (covered in the earlier dictionaries article) is the natural way to check for specific optional settings, with a sensible fallback if the caller didn't provide them:

def connect(**kwargs):
    timeout = kwargs.get("timeout", 30)
    retries = kwargs.get("retries", 3)
    print(f"Connecting with timeout={timeout}, retries={retries}")

connect()                              # Connecting with timeout=30, retries=3
connect(timeout=10)                    # Connecting with timeout=10, retries=3
connect(timeout=10, retries=5)         # Connecting with timeout=10, retries=5

This gives callers the flexibility to override only the specific settings they care about, while everything else quietly falls back to a reasonable default.

Combining Both, and the Strict Ordering Rules

Legal signature order

As covered in the previous article on function arguments, Python enforces a strict order when a function signature combines several parameter types: standard positional parameters → *args → keyword-only parameters → **kwargs.

def example(a, b, *args, c=10, **kwargs):
    print(a, b, args, c, kwargs)

example(1, 2, 3, 4, c=99, extra="hello")
# 1 2 (3, 4) 99 {'extra': 'hello'}

Walking through that call: a and b claim the first two positional arguments, 3 and 4 get swept into args as a tuple (since there are no more named positional parameters to fill), c is explicitly overridden via keyword, and extra="hello" — not matching any named parameter — gets packed into kwargs.

Why reversing the order raises a SyntaxError
def broken(**kwargs, *args):   # SyntaxError — wrong order
    pass

**kwargs has to come last in a function definition, because it's designed to catch everything else that wasn't matched by anything defined before it — there's nothing meaningful left for Python to place after it. Similarly, *args needs to come before any keyword-only parameters, since those keyword-only parameters rely on *args having already "used up" its share of the positional arguments before they're addressed by name.

Real-World Use Cases and When to Avoid Them

Wrapper and decorator functions
This is arguably the single most common legitimate use of *args/**kwargs together: writing a function that wraps another function, without needing to know or duplicate that other function's exact parameter list.
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"{func.__name__} took {elapsed:.4f} seconds")
        return result
    return wrapper

@timer
def slow_add(a, b):
    time.sleep(1)
    return a + b

slow_add(3, 5)
# slow_add took 1.0002 seconds

Here, wrapper(*args, **kwargs) can forward any combination of arguments through to func, regardless of what func's actual signature looks like — a genuinely essential pattern for decorators (a topic covered in more depth in a later article in this series), logging wrappers, timing utilities, and caching layers, all of which need to transparently pass arguments through to whatever function they're wrapping.

Framework and library patterns

You'll also see *args/**kwargs used extensively in libraries and frameworks that need to pass extra, optional configuration through multiple layers of function calls without every intermediate layer needing to know about every possible option in advance. This lets a library add new configuration options over time without breaking the signatures of functions that merely pass those options along.

Honest guidance: don't overuse this

It's worth being direct about a real tradeoff here: *args/**kwargs genuinely reduce readability and hurt IDE autocompletion compared to explicit, named parameters. When you call a function defined with plain named parameters, your editor can tell you exactly what's expected and catch a typo in a keyword name immediately. When a function just accepts **kwargs, that safety net disappears — the editor has no way to know what keys are actually meaningful, and a typo in a keyword argument silently gets swallowed into the kwargs dictionary rather than raising a helpful error.

The practical rule of thumb: use explicit, named parameters whenever the set of arguments a function needs is fixed, known, and reasonably small — which covers the overwhelming majority of functions you'll write. Reach for *args/**kwargs specifically when you need genuine flexibility — an unknown or highly variable number of arguments, or a wrapper function forwarding arguments to something else without needing to inspect them. Using **kwargs as a substitute for just writing out a function's actual expected parameters, purely to "future-proof" it or avoid deciding on a signature, tends to produce code that's harder to use correctly, not more flexible in any way that matters.

PREVIOUSNEXT LESSON