Intermediate 4 min · March 05, 2026

*args and **kwargs — Silent Typo Bugs in Python

A 'emai' typo passed through **kwargs for 8 months, corrupting user records with defaults.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
Quick Answer
  • *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 typosfunc(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.

*args vs **kwargs in Python
Feature / Aspect*args**kwargs
What it collectsExtra positional argumentsExtra keyword arguments
Python type at runtimetupledict
Argument style at call sitefunc(1, 2, 3)func(a=1, b=2, c=3)
How to iteratefor item in argsfor key, value in kwargs.items()
Order preserved?Yes — insertion orderYes — insertion order (Python 3.7+)
Unpack into callfunc(*my_list)func(**my_dict)
Position in signatureAfter regular paramsAlways last
Primary use caseVariadic positional data (scores, prices)Named config / metadata forwarding
Typo detectionN/A — positional, no namesSilent — typos become valid keys
Can be used alone?YesYes

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: Use if '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
    The decorator must define a wrapper with args, kwargs to capture any arguments, and call the original function with args, *kwargs to forward them. Use functools.wraps(func) to preserve metadata like __name__ and __doc__. Inside the wrapper, I'd log the args and kwargs before calling, then the result after. Example: def log_calls(func): @functools.wraps(func) def wrapper(args, *kwargs): print(f'Calling {func.__name__} with args={args} kwargs={kwargs}'); result = func(args, **kwargs); print(f'{func.__name__} returned {result}'); return result; return wrapper. This works on any function because it captures everything and forwards everything. The key insight is that the wrapper can't know the signature of the wrapped function at write time, so it must be generic.
  • 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
    They're opposite operations. In function definition, def f(args): means 'collect all remaining positional arguments into a tuple named args'. That's tuple packing. At the call site, f([1,2,3]) means 'unpack the iterable into positional arguments'. That's tuple unpacking. They're inverses: definition packs, call unpacks. The same symbol is used for both, but the context (definition vs call) determines the behavior. You can also use * for dicts in both contexts: definition packs, call unpacks. Understanding this duality is key to using argument forwarding correctly.
  • QWhat happens if I call my_func(*{'timeout': 30, 'retries': 3}) and my function signature is def my_func(args, **kwargs). What will args and kwargs contain? What if I accidentally pass a positional argument at the same time?Mid-levelReveal
    With def my_func(*args, kwargs), the call my_func({'timeout':30, 'retries':3}) results in args = () (empty tuple) and kwargs = {'timeout':30, 'retries':3}. All keyword arguments go into kwargs. If you call my_func(1, 2, {'a':1}), then args = (1, 2) and kwargs = {'a':1}. Positional arguments before the unpacked dict go into args, keyword-dict items go into kwargs. The order matters: my_func(1, {'a':1}, 2) is a SyntaxError (positional arguments cannot follow keyword dict unpacking). This is the same rule as normal keyword arguments: once you start using keywords, all remaining arguments must be keyword-style.
  • QHow would you catch typos in **kwargs at development time without runtime overhead in production?SeniorReveal
    Static type checking with mypy and TypedDict. Define ExpectedKwargs = TypedDict('ExpectedKwargs', {'email': str, 'name': str}, total=False). Annotate your function as def create_user(kwargs: ExpectedKwargs) -> User. Now mypy will catch create_user(emai='x') as an error because 'emai' is not a key in ExpectedKwargs, with no runtime cost. For validation at runtime in production, you can add a wrapper that checks keys against an allowed set: allowed = {'email', 'name'}; if extra := set(kwargs) - allowed: raise TypeError(f'Unexpected keys: {extra}'). Another approach: replace kwargs with a dataclass parameter. The dataclass fields are explicit, type-checkable, and catch typos on construction. For public APIs, avoid **kwargs entirely — use explicit parameters or a dataclass.

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

Previous
Functions in Python
2 / 11 · Functions
Next
Lambda Functions in Python