*args and **kwargs — Silent Typo Bugs in Python
A 'emai' typo passed through **kwargs for 8 months, corrupting user records with defaults.
- *args collects extra positional arguments into a tuple; **kwargs collects keyword arguments into a dict
- Core use cases: decorators (forward unknown args), variadic functions (log, sum), base classes (subclass extension)
- Performance: Tuple packing/unpacking is O(n) but negligible for typical arg counts (<100 elements)
- Production trap: **kwargs silently swallows typos —
func(user_idd=123)passes{'user_idd': 123}instead of raising TypeError - Biggest mistake: Using **kwargs when your interface is fixed — explicit parameters catch typos immediately, kwargs hide them
Most Python tutorials show you args and kwargs as a syntax curiosity — a footnote after the 'real' stuff. That's backwards. These two features are why Python's standard library, every major framework, and every well-designed API you've ever touched actually works. Django's class-based views use kwargs to pass URL parameters. Python's built-in print() uses args so you can pass it one string or twenty. Decorators — the feature that powers Flask routes, pytest fixtures, and logging middleware — are almost impossible to write correctly without them.
But here's what tutorials miss: **kwargs hides typos. A misspelled keyword argument won't raise a TypeError — it'll just silently add a new key to the dict, and your function will ignore it (or worse, treat it incorrectly). I've debugged production incidents where a config value wasn't applied because someone wrote timeuut=30 instead of timeout=30. The function kept running, no error, just wrong behavior.
By the end you'll understand not just what args and *kwargs do, but when to use them, when to avoid them, and how to debug the silent failures they can create. You'll see real code from Django, Flask, and functools.wraps — the patterns that separate intermediate from senior Python developers.
Why Fixed-Argument Functions Break in the Real World
When you define a function like def add(a, b), you're making a promise: this function always takes exactly two things. That's fine for a calculator. It's not fine for the real world, where requirements shift.
Picture a logging utility. On day one you log a message and a level. On day two someone needs to attach a user ID. Day three, a request ID. If your function signature is rigid, you're back editing it every time. You'd end up with def log(message, level, user_id=None, request_id=None, session_id=None) — a parameter list that grows forever.
args and kwargs exist to solve exactly this. They're not shortcuts or hacks — they're a deliberate design pattern that says 'this function is designed to receive a variable number of inputs.' Understanding when to reach for them (and when not* to) is what separates intermediate from advanced Python.
How *args and **kwargs Actually Work Under the Hood
The asterisks aren't magic keywords — they're unpacking operators. When Python sees args in a function definition, it's an instruction: 'collect all remaining positional arguments into a tuple and bind that tuple to the name args.' The name itself is just a convention. You could write toppings or *scores and it would work identically.
Similarly, `**kwargs` says: 'collect all remaining keyword arguments into a dictionary.' The double asterisk means 'key-value pairs,' not just 'things.'
This matters because a tuple and a dictionary behave differently. You iterate over args with a simple for loop. You iterate over *kwargs with .items() to get both the key and the value. You can also use len(), slicing, and unpacking on args just like any tuple. kwargs gives you .get(), .keys(), and the full dict API.
The order of parameters in a function signature is strict: regular positional params first, then args, then keyword-only params, then *kwargs. Breaking this order raises a SyntaxError immediately.
The Pattern That Powers Real Frameworks: Decorator Forwarding
Here's where args and *kwargs stop being a curiosity and become genuinely essential. Decorators — the @something syntax you see on Flask routes, Django views, and test functions — work by wrapping one function inside another. The wrapper needs to call the original function. But the wrapper doesn't know what arguments the original takes.
This is the single most important real-world use case: writing a wrapper function that forwards every argument to the inner function without caring what those arguments are.
Without args and *kwargs, you'd have to write a different decorator for every possible function signature. With them, you write one decorator that works universally. This is also the pattern behind Python's functools.wraps, unittest.mock.patch, and virtually every middleware system you'll encounter in production code.
Once you understand this pattern, reading framework source code stops feeling like magic and starts feeling like recognizable building blocks.
When NOT to Use *args and **kwargs
Using args and *kwargs everywhere is a code smell, not a sign of skill. If your function always takes exactly two user IDs to compare, define it that way. Explicit signatures are self-documenting — they tell the caller exactly what's expected. IDEs can autocomplete them. Type checkers can validate them.
**kwargs in particular can hide bugs. If you misspell a keyword argument, Python won't raise a TypeError — it'll just silently add the misspelled key to the dict and your logic will skip it. With explicit parameters, Python catches the typo immediately.
The right time to use them: when you're genuinely wrapping or forwarding calls (decorators, middleware), when you're building a function that is intentionally variadic by design (like a custom print or log function), or when you're writing a base class method that subclasses will extend with their own signatures. The wrong time: when you're just being lazy about writing out three parameters.
| Feature / Aspect | *args | **kwargs |
|---|---|---|
| What it collects | Extra positional arguments | Extra keyword arguments |
| Python type at runtime | tuple | dict |
| Argument style at call site | func(1, 2, 3) | func(a=1, b=2, c=3) |
| How to iterate | for item in args | for key, value in kwargs.items() |
| Order preserved? | Yes — insertion order | Yes — insertion order (Python 3.7+) |
| Unpack into call | func(*my_list) | func(**my_dict) |
| Position in signature | After regular params | Always last |
| Primary use case | Variadic positional data (scores, prices) | Named config / metadata forwarding |
| Typo detection | N/A — positional, no names | Silent — typos become valid keys |
| Can be used alone? | Yes | Yes |
Key Takeaways
- args is a tuple at runtime — iterate it, slice it,
len()it like any tuple. *kwargs is a dict — use .items(), .get(), and .keys() on it. - The universal decorator pattern (wrapper(args, kwargs) → func(args, **kwargs)) is the single most important real-world use of these features. Master it.
- kwargs silently swallows typos — if your function's interface is fixed and known, use explicit named parameters instead. Save kwargs for genuine forwarding scenarios.
- The and * operators work in BOTH directions: in function definitions they collect arguments into tuple/dict; at the call site they unpack tuple/dict back into arguments. Same symbol, opposite direction.
Common Mistakes to Avoid
- Putting **kwargs before *args in a function signature
Symptom: SyntaxError: invalid syntax immediately when trying to define or call the function.
Fix: Always follow the order: regular params → args → keyword-only params → kwargs. Python enforces this strictly. Example:def f(a, b,args, c=3, **kwargs): - Mutating the kwargs dict inside the function and expecting caller's dict to update
Symptom: Caller's dict remains unchanged after function returns. Modifications to kwargs inside the function seem lost.
Fix: **kwargs creates a new dict from the caller's keyword arguments. The caller's original variables are untouched. If you need to pass mutations back, return the modified dict explicitly:return updated_kwargs. - Passing a dict as a positional argument instead of unpacking it
Symptom: Calling `func({'key': 'value'})` when you meant `func(**{'key': 'value'})` sends the whole dict as one positional arg into *args, not as keyword args. The symptom is `args = ({'key': 'value'},)` and kwargs is empty.
Fix: Use to unpack dicts into kwargs:func(my_dict). If you need to pass a dict as a single positional argument, that's fine — but recognise the difference. - Missing *args/**kwargs in wrapper functions causing signature mismatch
Symptom: Decorator works on functions with no arguments but fails on functions with arguments: `TypeError: wrapper() takes 0 positional arguments but X were given`.
Fix: Your wrapper must accept args and kwargs and forward them:def wrapper(args, *kwargs): return original(args, **kwargs). Without this, the wrapper has a fixed signature and will break on any argument passage. - Using kwargs.get(key) when key might be present with value False or None
Symptom: You pass `flag=False` (valid config), but inside `if kwargs.get('flag'):` treats False as missing and falls back to default True. The config is incorrectly applied.
Fix: Useif 'flag' in kwargs:to check existence, not.get()truthiness. Or provide a sentinel:value = kwargs.get('flag', _SENTINEL); if value is not _SENTINEL:. For flags, consider explicit parameters or a dataclass.
Interview Questions on This Topic
- QWalk me through how you'd write a decorator that logs the arguments and return value of any function it wraps, regardless of that function's signature.SeniorReveal
- QWhat's the difference between defining a function with args and calling a function with the operator? They look similar — are they the same thing?Mid-levelReveal
- QWhat happens if I call
my_func(*{'timeout': 30, 'retries': 3})and my function signature isdef my_func(args, **kwargs). What will args and kwargs contain? What if I accidentally pass a positional argument at the same time?Mid-levelReveal - QHow would you catch typos in **kwargs at development time without runtime overhead in production?SeniorReveal
Frequently Asked Questions
Do I have to name them 'args' and 'kwargs'?
No — the names are pure convention. The operators are and . You can write def func(scores, **player_data) and it works identically. That said, stick to args and kwargs unless you have a specific reason not to — every Python developer recognises them instantly.
Can I use *args and **kwargs together with regular parameters?
Yes, but the order is strict: regular positional params come first, then args, then any keyword-only params (params defined after args), then *kwargs. Breaking this order raises a SyntaxError. Example: def func(name, age, scores, active=True, **metadata) is valid.
Why does modifying kwargs inside my function not change the original dict I passed in?
Because kwargs doesn't receive your dictionary — it receives a new dictionary built from the keyword arguments at the call site. The double-star operator unpacks key-value pairs; it doesn't pass a reference to your original dict. If you pass my_config, modifying kwargs inside the function leaves my_config unchanged.
How do I require certain keys when using **kwargs?
You can't enforce required keys with kwargs alone. You have to check manually: if 'required_key' not in kwargs: raise TypeError('missing required_key'). That's why explicit parameters (def f(required, kwargs):) are better for required arguments. Use **kwargs only for optional extras, never for essential ones.
That's Functions. Mark it forged?
4 min read · try the examples if you haven't