skip to content

questions

3

What do *args and **kwargs collect in a Python function definition?

level: juniorimportance: must knowfreq 82%

answer

  1. Two stars, two containers
  2. One bucket by position, one by name
  3. Fixed types, empty rather than absent
  4. tuple for *args, dict for **kwargs

basics

~10 s

*args collects the leftover positional arguments into a tuple; **kwargs collects the leftover keyword arguments into a dict. Both types are fixed, and both are empty rather than None when nothing extra is passed.

solid answer

~40 s

In a `def`, the single star and double star declare **variadic parameters**. Any positional argument not taken by a declared parameter is packed into a `tuple` bound to `args`; any keyword argument whose name matches no declared parameter is packed into a `dict` bound to `kwargs`. The types never vary: on a call with nothing extra they are `()` and `{}`, never `None`. The names are pure convention — `def f(*items, **options)` is identical. A signature may hold at most one of each, `*args` after the ordinary parameters and `**kwargs` last of all; breaking that is a `SyntaxError`. `kwargs` keys must be strings, and since Python 3.6 (PEP 468) it preserves the order the keywords were written at the call site.

code

python · 5 lines
python
def show(*args, **kwargs):
    return type(args).__name__, args, type(kwargs).__name__, kwargs

print(show(1, 2, mode="fast"))
print(show())

go deeper

for a junior

Be ready to state the two types instantly, without hedging: *args is a tuple, **kwargs is a dict, and both are empty containers rather than None on a bare call. Then show it in three lines of code.

for a middle

Explain the mechanics behind the types: how the declared parameters are filled first and the leftovers packed, that at most one of each may appear with **kwargs last, that a violation is a compile-time SyntaxError, and that kwargs keys are strings kept in call order.

for a senior

Show judgement about when a variadic signature is warranted at all. An interviewer expects you to name what it costs — no arity check, no editor help, no useful static typing for callers — and to say which boundaries in your code deserve real parameters instead.

for a principal

Own the API-design angle: a permissive signature is a contract you cannot tighten later without breaking callers. Be ready to argue where variadics belong in a shared library and where a named, evolvable parameter list serves the organisation better.

### The two collectors A Python call site produces exactly two kinds of argument: **positional** arguments, written by position (`f(1, 2)`), and **keyword** arguments, written `name=value` (`f(mode="fast")`). A function definition binds those to its declared parameters. Two extra parameter forms exist to catch whatever the declared parameters did not take: * `*args` is a **variadic positional** parameter. Every positional argument left over after the declared parameters are filled is packed into a **`tuple`** and bound to that name. * `**kwargs` is a **variadic keyword** parameter. Every keyword argument whose name matches no declared parameter is packed into a **`dict`** and bound to that name. The stars are the syntax; `args` and `kwargs` are only a very strong convention. `def f(*items, **options)` behaves identically, and inside the body you use the names you chose. ### The types are fixed, and never `None` This is the point interviewers actually probe. `args` is *always* a `tuple` and `kwargs` is *always* a `dict`, on every call, including a call that supplies nothing extra — then they are the empty tuple `()` and the empty dict `{}`. Code that writes `if args is None` is testing a state that cannot occur; the real test is `if args:` or `if not kwargs:`. Because `args` is a tuple it is immutable: you can index and iterate it, but you cannot `append` to it. If you need to adjust the collected arguments before passing them on, build a `list` from it. `kwargs`, being a `dict`, is mutable, and it is a **fresh dict created for that call** — popping a key out of it does not disturb any mapping the caller happened to unpack into the call. ### What `kwargs` may contain Keyword argument names are identifiers, so the ordinary way to fill `kwargs` produces identifier-like string keys. The keys must be strings: passing a mapping with a non-string key raises `TypeError: keywords must be strings`. Strings that are *not* valid identifiers can still get in through a mapping — `f(**{"a-b": 1})` binds `{"a-b": 1}` into `kwargs` — which is why treating `kwargs` as a plain dict of strings, rather than assuming every key is a legal attribute name, is the safe habit. Since Python 3.6 (PEP 468) `kwargs` preserves the order in which the keyword arguments were written at the call site, and from 3.7 that ordering is a language guarantee for every `dict`. So `f(b=1, a=2)` yields `{'b': 1, 'a': 2}`, not an alphabetised or arbitrary mapping. ### Where they may appear A signature may carry **at most one** `*args` and **at most one** `**kwargs`. `*args` comes after the ordinary parameters; `**kwargs` must be the **last** parameter in the definition. Writing a second star parameter, or putting anything after the double-star one, is a `SyntaxError` raised at compile time, not a runtime error — `def f(*a, *b)` reports "* argument may appear only once", and `def f(**a, b)` reports "arguments cannot follow var-keyword argument". Note also what a variadic parameter does *not* accept. `def f(**kw)` declares no positional parameters at all, so `f(1)` raises `TypeError: f() takes 0 positional arguments but 1 was given`. The double star collects keywords only; the single star collects positionals only. They are two separate buckets, and an argument goes into exactly one of them. ### Why they exist Three recurring jobs: 1. **Genuinely variadic APIs** — a function that formats, joins, sums or dispatches over "as many as you like" of something. 2. **Forwarding** — a wrapper that must relay a call to another function without restating that function's parameter list. 3. **Open option sets** — a boundary that accepts a bag of settings whose membership it does not want to enumerate. ### The cost A variadic signature is maximally permissive, and permissiveness is not free. `def f(*args, **kwargs)` tells a reader, an editor and a static type checker nothing about what the function actually takes; the useful arity error moves from the call site into the body or into a callee one frame deeper. And because `**kwargs` has a home for *every* keyword name, a caller's typo is no longer an error — it is simply an unread entry in a dict. Use variadics where the argument set is genuinely open, and declare real parameters everywhere else.

  • Are the names args and kwargs required by the language?
    No. The stars are the syntax; the names are convention. `def f(*items, **options)` behaves exactly the same, and inside the body you use whichever names you chose. Following the convention still matters for readability, because every reader recognises it instantly.
  • Can you append to args inside the function body?
    No — `args` is a `tuple`, so it is immutable. Build a `list` from it if you need to adjust the collected positional arguments before passing them on. `kwargs` is a `dict` and is mutable, and it is a fresh dict created for that call, so popping from it never disturbs a mapping the caller unpacked into the call.
  • What happens if you call a function declared as def f(**kw) with a positional argument?
    It raises `TypeError: f() takes 0 positional arguments but 1 was given`. The double star collects keyword arguments only; it declares no positional parameters at all. The two stars fill two separate buckets, and an argument lands in exactly one of them.

A parcel sorter with two bins: unlabelled parcels go into a sealed tray in arrival order (the tuple), and labelled ones go into a pigeonhole rack keyed by name (the dict).

saying these in an interview costs you the question

  • Says *args is a list you can append to
  • Thinks **kwargs is a list of name/value pairs
  • Claims args or kwargs is None when nothing extra is passed
  • Believes the language requires the names args and kwargs
  • Thinks a signature may declare two *args parameters
  • Assumes **kwargs can hold non-string keys

context

open as a page

How does the * in a Python def differ from the * in a call like f(*args)?

level: middleimportance: should knowfreq 60%

basics

~20 s

In a def the star packs: leftover arguments are collected into a tuple or dict. In a call the star unpacks: the iterable or mapping is spread back out into separate arguments. Chaining both forwards a whole call.

open as a page

Why does a function taking **kwargs silently ignore a misspelled keyword, and how do you stop it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

An 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.

open as a page