Why does a function taking **kwargs silently ignore a misspelled keyword, and how do you stop it?
answer
- A signature that accepts everything
- No parameter missing, so no complaint
- The typo survives as an unread entry
- Pop what you know, reject the remainder
basics
~20 sAn unexpected-keyword TypeError fires only when no parameter can hold the name, and **kwargs holds every name. If the function reads the keys it wants and drops the rest, the typo becomes an unread dict entry. Drain the leftovers and raise.
solid answer
~40 s`TypeError: got an unexpected keyword argument` is raised only by a function with no home for that name — and `**kwargs` is a home for every name, so the check is switched off at that boundary. If the function then reads what it wants with `dict.get` and discards the rest, or forwards into another variadic sink, the misspelling is never an error; it is just data. A feature-flag helper `def is_enabled(flag, **options)` reading `options.get("default", False)` returns `False` for `deafult=True` and logs nothing. Fixes, in order of preference: declare real parameters instead of a bag; forward into a strict callee so *it* validates; or drain the bag yourself — `pop` the keys you know, then `raise TypeError` if anything remains. Back it with one test that passes a bogus keyword and asserts the error.
code
python · 4 linesdef is_enabled(flag, **options):
return options.get("default", False)
print(is_enabled("locale-aware-numbers", deafult=True))go deeper
Take away one fact: a function declared with **kwargs accepts any keyword name at all, so a spelling mistake in a keyword argument may produce no error whatsoever. Try it in the REPL until the silence stops surprising you.
Explain the mechanism precisely: the unexpected-keyword TypeError comes from binding, and a variadic keyword parameter gives every name a binding. Then show the guard - pop the keys you understand and raise on whatever is left.
Demonstrate that you have debugged this in a running system. Describe how a swallowed option produces a plausible wrong value rather than a crash, how you would trace it back, and which of forwarding, draining or declaring real parameters you would apply where.
Own the policy question: which boundaries in the system are permitted a variadic keyword parameter at all, and what every one of them must do with the leftovers. Argue why 'accepted and ignored' is worse than either rejecting or honouring an option.
### Why the typo is silent Python raises `TypeError: ... got an unexpected keyword argument 'x'` only when the function being called has **no home** for the name `x`. A `**kwargs` parameter is a home for *every* name. So the moment a signature carries a variadic keyword parameter, unknown-keyword detection is not weakened — it is **switched off** at that boundary and deferred to whoever downstream still has a strict signature. If nothing downstream does, the misspelled keyword never becomes an error. It becomes **data**: one unread entry in a dict that the function will discard when it returns. ### The shape it takes in production A feature-flag service ships a small client helper: ```python def is_enabled(flag, **options): return options.get("default", False) # plus a real lookup ``` A nightly export job — the one holding a 2.4 GB working set while it formats a report — calls `is_enabled("locale-aware-numbers", deafult=True)`. There is no exception, no warning, no log line. `options` is `{"deafult": True}`, `options.get("default", False)` returns `False`, the locale-aware formatter never switches on, and the export ships with a locale-dependent number format that is wrong for every non-English region. The defect is discovered months later by a human reading a spreadsheet, because the only artefact of the bug is a correct-looking `False`. Three variants of the same failure, worth naming in an interview: 1. The variadic function **reads keys itself** with `dict.get` and drops the rest — the case above. 2. The chain **ends in another `**kwargs` sink** — a config dict, a JSON payload, a logging extra — so no strict signature is ever reached. 3. The keyword is **shadowed by a default**: the parameter the caller meant to override has a perfectly reasonable default, so the wrong behaviour is plausible rather than obviously broken. ### The case where it *does* raise Forwarding into a strict callee restores the check: ```python def is_enabled(flag, **options): return evaluate(flag, **options) # evaluate declares real parameters ``` Now `evaluate` receives `deafult=True`, has no such parameter, and raises `TypeError`; CPython 3.14 even appends a closest-match suggestion ("Did you mean 'default'?"). The lesson is precise: **forwarding preserves the error, consuming destroys it.** ### How to prevent it * **Do not use `**kwargs` unless the option set is genuinely open.** Most functions that take one could name three parameters instead. A variadic signature you added "for flexibility" buys nothing and disables a real check for every future caller. * **Drain the bag.** If you must accept `**options`, `pop` the keys you understand and then refuse what is left: `if options: raise TypeError(f"unexpected keyword arguments: {sorted(options)}")`. Three lines, and the boundary behaves like a normal function again. * **Forward rather than consume.** Relay into the function that owns the real signature and let it validate; do not re-implement its option handling with `dict.get`. * **Type the signature.** A static type checker flags an unknown keyword at a precisely-typed call boundary, but it has nothing to check against when the signature is `**kwargs` typed as `object`. * **Test the negative case.** One test that calls the boundary with a bogus keyword and asserts a `TypeError` locks the behaviour in; without it, "silently ignores unknown options" is untested and therefore permanent. * **If you truly cannot raise, log.** Emitting a warning naming the unknown keys is far worse than raising and far better than silence, and it gives operations something to grep. ### One place the language drains the bag for you Constructor chains are the notable exception. If every `__init__` in a cooperative chain accepts `**kwargs` and relays it with `super().__init__(**kwargs)`, the leftovers eventually reach `object.__init__`, which accepts no extra arguments and raises `TypeError: object.__init__() takes exactly one argument (the instance to initialize)`. That is the same rule stated once more — the check happens at the first binding with no home for the name — and it is worth knowing because it explains an error message that otherwise looks like it came from nowhere. Break the chain by consuming the bag before the top, and the diagnostic vanishes again. That also illustrates why the traceback is unhelpful even in the cases that *do* raise: the exception is reported at the frame that finally bound the arguments, which may be several relays away from the caller who wrote the typo. The fix is the same as the prevention — validate at the boundary the caller actually touched, so the error names the function the caller actually called. ### The judgement to voice The instinct to blame the caller for the typo is the wrong instinct: the caller wrote something the API accepted. **A permissive signature is an API decision**, and "accepted and ignored" is the worst of the three possible behaviours — worse than rejecting, and worse than honouring. Senior work here is deciding which boundaries in a system are allowed to be variadic at all, and making every one that is either forward its bag to a strict consumer or validate it itself.
- Why does forwarding the same **options into a strict callee raise, while consuming them does not?Because the check belongs to whichever function finally binds the arguments. Forwarding hands the bag to a function with real parameters, which finds no home for `deafult` and raises `TypeError`. Consuming the bag with `dict.get` means no such function is ever reached, so nothing can complain. Forwarding preserves the error; consuming destroys it.
- How would you detect this class of bug in an existing codebase?Grep for variadic keyword parameters at public boundaries and check whether each one forwards its bag or consumes it. For every consumer, add a test that passes an unknown keyword and asserts a `TypeError`. A static type checker helps only where the boundary has a precise signature, which is exactly what a bare `**kwargs` removes.
- When is silently ignoring an unknown keyword actually the right behaviour?Almost never inside one codebase. It is defensible only where the bag is genuinely open and forward-compatible - relaying options to a component whose set of settings you do not control, or accepting a payload from an older or newer peer. Even then, log the unrecognised keys rather than dropping them without trace.
saying these in an interview costs you the question
- Insists Python always raises TypeError on a misspelled keyword
- Treats **kwargs as if it validated its own keys
- Adds **kwargs everywhere for flexibility
- Consumes the bag with dict.get and never checks the remainder
- Blames the caller instead of the permissive signature
- Thinks a static type checker catches it through a bare **kwargs