*args and **kwargs: two stars with different prices — and one of them makes unknown names acceptable
*args is a tuple, **kwargs is a dict, and the resemblance ends there. Passing one star is nearly free; passing two costs four times as much. And a **kwargs added for flexibility turns a misspelled argument name from a TypeError into a silently accepted default — throwing away the hint the interpreter learned to give in 3.13.
Full technical treatment
TL;DR
A star does opposite things on the two sides of a call. In a declaration it
collects: one star gathers the surplus positional arguments into the tuple
args, two stars gather the surplus keyword arguments into the dict kwargs.
In a call it spreads: one star lays a sequence out as separate positional
arguments, two stars lay a dict out as separate keyword arguments. The
collected objects arrive even when there is nothing to deliver: a call with no
arguments produces () and {} inside, not None.
Hence the main consequence: two stars added to a signature "for flexibility"
make unknown keyword names acceptable to that function — there is now
somewhere to put them. The call connect("db", timeuot=5) stops being a TypeError and
quietly lands in options, while timeout stays equal to 30. The second
consequence is price, and the two stars do not share it: plain(*args) is
nearly free (×1.1–1.3 of a direct call), plain(**kwargs) costs four times
and more (×4.0–5.2). Stable across all four versions; three decorator layers
come to ×8.5–11.9, so eight ninths of the time goes into getting to the work
rather than doing it.
Beyond that: numbers, versions and boundaries. The () and {} are
semantics, not a claim about allocation: the empty tuple in CPython is shared by
everyone, and how much gets allocated is a question for the measurement below,
not for the language. What becomes acceptable is exactly the unknown names; the
rest of the binding rules stay where they were — a run of
bench/args-kwargs/binding_rules.py puts each of them through a real call:
strict(a=1, unknown=9) TypeError
loose(a=1, unknown=9) a=1 b=2 kwargs={'unknown': 9}
loose() TypeError ← required parameter
loose(1, a=2) TypeError ← passed twice
positional_only_loose(a=1, b=2, c=3) TypeError ← a name instead of a position
The last line is why the / in a signature is not decoration. And the run
prints the most surprising case separately: for def f(a, /, b, *, c, **kwargs)
the call f(1, b=2, c=3, a=99) is not an error — a=99 goes into kwargs,
because a positional-only parameter does not reserve its name for keyword
arguments.
Worse: in 3.13 the interpreter learned to suggest the right name — "Did you mean
'timeout'?". A function with **kwargs throws that help away and behaves on
3.14 exactly as it did on 3.11.
The / from PEP 570 is not decoration: while a parameter can be passed by name,
that name is taken, and a key of the same name cannot reach **kwargs. This is
how dict itself is declared: its signature is ($self, /, *args, **kwargs).
- how a function with parameters is declared, and how it is then called;
- the difference between passing by position and passing by name:
f(1, 2)againstf(a=1, b=2); - that a list and a dict are containers, and that a dict holds name-to-value pairs.
co_argcount,co_kwonlyargcount,CALL_FUNCTION_EX,BUILD_MAP,DICT_MERGE;- why signatures sometimes contain
/or a bare*,functools.wraps,Unpack[TypedDict].
Base: a star on the two sides of a call
A star has several roles in Python, and all of them are written with the same
character. Until the roles are pulled apart, any conversation about *args and
**kwargs goes round in circles, so let us pull them apart first.
What tells the roles apart is the side: a star means one thing where a function is declared and the opposite where it is called.
In a declaration a star collects. It takes whatever found no parameter of its own and puts it under a single name:
- one star collects the surplus positional arguments — the ones passed by position;
- two stars collect the surplus keyword arguments — the ones passed by name.
In a call a star spreads. It takes a ready container and supplies its contents as separate arguments:
- one star spreads a sequence into positional arguments:
f(*seq)is the same thing asf(seq[0], seq[1], …); - two stars spread a dict into keyword arguments:
f(**mapping)isf(name=value, …)for every pair.
Here are those roles side by side, with a last row for the form that uses the star quite differently:
| form | where it stands | what it does |
|---|---|---|
def f(*args, **kwargs) | declaration | packs the surplus arguments into a tuple and a dict |
f(*seq) | call | unpacks a sequence into positional arguments |
f(**mapping) | call | unpacks a mapping into keyword arguments |
def f(a, /, b, *, c) | declaration | packs nothing: / and a bare * only divide the parameters into kinds |
Four different operations, and the confusion between the first two is the
commonest: f(*args) inside a wrapper does not build a tuple, it takes apart
one that already exists. The last row has nothing to do with collecting at all,
and it gets a section of its own further down.
And here is the question the rest of the lesson answers: what does that collecting cost? It is written with one character and looks free — but it has a price in time and, on top of that, a consequence time does not measure.
That is already enough to answer the basic interview question. Everything below is about what arrives being a tuple and a dict rather than something arbitrary; about two stars costing four times what one costs at the call site; and about a name the function does not have ceasing to be an error the moment two stars appear in the declaration.
Mechanism 1: what these objects are
*args is a tuple, **kwargs a dict, and the argument-binding rules do not depend on the version.Start with what one function settles.
def collect(*args, **kwargs):
return type(args).__name__, type(kwargs).__name__collect(1, 2, x=3) returns ('tuple', 'dict'). Not a list — a tuple, and
one line settles it: args.append(3) gives
AttributeError: 'tuple' object has no attribute 'append'. There will be no
mutable container here, however many arguments arrive.
The second and less obvious part: the objects are created even when there is nothing to deliver.
def probe(*args, **kwargs):
return args, kwargs
probe() # -> ((), {})Not None, but an empty tuple and an empty dict. The language reference puts it
exactly so: *identifier "is initialized to a tuple receiving any excess
positional parameters, defaulting to the empty tuple". So if args: is the
right check, while if args is not None: is always true.
What the function's own code knows about the stars
def full(a, b=2, *args, c, d=4, **kwargs):
passThe signature looks tangled, but the interpreter breaks it into three independent numbers and two flags:
| field | value | what it means |
|---|---|---|
co_argcount | 2 | a and b are ordinary: positionally or by name |
co_posonlyargcount | 0 | nothing before a / |
co_kwonlyargcount | 2 | c and d are keyword-only |
CO_VARARGS | True | there is a *args |
CO_VARKEYWORDS | True | there is a **kwargs |
From which follows the simple fact that clears up the main confusion: c and
d became keyword-only not because they have defaults but because they stand
after *args. That is the rule from PEP 3102, and a bare * in the signature
does the same without collecting a tuple.
The order of keyword arguments is preserved: order(z=1, a=2, m=3) yields keys
as ['z', 'a', 'm'], not sorted.
Mechanism 2: the bug that never crashes
A connection function. Three understandable parameters, and someone added a fourth — "in case something needs passing through to the driver".
def connect(host, port=5432, timeout=30, **options):
...Six months later, in another file, someone writes a call:
connect("db", timeuot=5)There is no error. timeuot is a legal key for **options, so the function
receives timeout = 30 while the 5 lands in options, where nobody looks. The
connection is made, the program carries on, and the difference shows up one day
under load — as a timeout nobody expected.
Without **options the same call fails at once, and fails informatively.
Why this got worse rather than better
In 3.13 the interpreter learned to suggest the right name. What's New says: "The error message now tries to suggest the correct keyword argument when an incorrect keyword argument is passed to a function". Verified on all four versions:
| version | connect("db", timeuot=5) without **options |
|---|---|
| 3.11.15 | got an unexpected keyword argument 'timeuot' |
| 3.12.3 | the same |
| 3.13.7 | ... 'timeuot'. Did you mean 'timeout'? |
| 3.14.7 | same as 3.13.7 |
So the language got better at catching this exact typo — and a function declared
with **kwargs throws that help away. With it, 3.14 behaves just like 3.11:
it says nothing.
What actually fixes it
Not vigilance. If **kwargs really is needed, then a check for unknown keys
where they are accepted:
KNOWN = {"sslmode", "application_name"}
def connect(host, port=5432, timeout=30, **options):
unknown = set(options) - KNOWN
if unknown:
raise TypeError(f"unknown parameters: {', '.join(sorted(unknown))}")Four lines restore the refusal on an unknown name — the one that stopped
happening by itself the moment those names became acceptable. And if **kwargs
was not needed, its absence is the check — for free.
Mechanism 3: / and the bare * — on taking names
Two things from PEP 570 and PEP 3102 that have a measurable consequence, not merely a cosmetic one.
The bare * forbids passing anything after it positionally:
def open_conn(host, *, timeout=30, retries=3):
...Now open_conn("db", 5) is an error rather than a five-second timeout that
arrived by counting. The order of parameters stops being part of the contract,
and can be changed without breaking callers.
The slash / does the opposite — it forbids passing by name. And it exists
not for symmetry but for a reason only a run makes visible:
def name_taken(name, **kwargs):
return name, kwargs
def name_free(name, /, **kwargs):
return name, kwargs| call | result |
|---|---|
name_taken("a", name="b") | TypeError: got multiple values for argument 'name' |
name_free("a", name="b") | ('a', {'name': 'b'}) |
While a parameter can be passed by name, that name is taken: a key of the
same name cannot reach **kwargs. PEP 570 names this very case: "A key scenario
is when a function accepts any keyword argument but can also accepts a
positional one".
This is how dict itself is declared. Its signature is visible from the
interpreter:
dict.__init__.__text_signature__ # ($self, /, *args, **kwargs)
dict(self="x") # {'self': 'x'}The slash releases the name self — without it, dict(self="x") would give
got multiple values for argument 'self'.
Mechanism 4: what a wrapper does to a signature
*args, **kwargs in a decorator has a side effect that is invisible in the
wrapper's own code.
def wrap(fn):
def inner(*args, **kwargs):
return fn(*args, **kwargs)
return innerinspect.signature reports (*args, **kwargs) for such a wrapper — the
parameter names are gone, and with them editor hints and half the value of type
checking. @functools.wraps(fn) cures that, and afterwards the signature shown
is the original: (host, port=5432, timeout=30).
But it does not describe what the wrapper will accept. Verified: the wrapper passes five positional arguments and any keyword name
straight through, checking nothing. The call then fails inside: connect() takes from 1 to 3 positional arguments but 5 were given. @wraps gives the signature
back to the tooling without making the wrapper obey it.
It shows in the traceback too: there is one frame more than there are layers — your own call frame plus one per wrapper.
| how it was called | frames in the traceback |
|---|---|
| directly | 1 |
| through one wrapper | 2 |
| through three | 4 |
The message always names the inner function — @wraps copied its name, so the
two cannot be told apart by text. The frames differ: without @wraps the
traceback shows three consecutive inner frames before the line with the typo.
Mechanism 5: typing — giving the erased names back
This is the only automatic way to restore what **kwargs erases.
Before 3.12, **kwargs could be annotated only with one type for all values.
PEP 692 names the limitation outright: "Currently **kwargs can be type hinted
as long as all of the keyword arguments specified by them are of the same type.
However, that behaviour can be very limiting". From 3.12 there is
Unpack[TypedDict]:
class Options(TypedDict, total=False):
sslmode: str
application_name: str
def connect(host: str, **options: Unpack[Options]) -> None:
...Now a type checker — mypy, pyright — sees names again and catches
timeuot=5 before the code runs: error: Unexpected keyword argument "timeuot". The call itself is unaffected: the interpreter still accepts it in
silence — to it, the name is acceptable. The refusal on an unknown name comes
back from outside, before the run, since inside the call it is gone.
Deeper: what the stars cost
Seven call forms measured back to back in one process: four are ways of passing
two numbers to plain, three do the same through a function with stars and
through wrappers.
The point here is the asymmetry. The two stars are written side by side, named in one breath, and priced entirely differently:
plain(*args)— ×1.1–1.3 of a direct positional call. Practically free.plain(**kwargs)— ×4.0–5.2. More than fourfold.
The reason is visible in the bytecode, and it is more specific than "a dict is more complicated than a tuple". Identical on all four versions:
plain(*ARGS) LOAD_GLOBAL LOAD_GLOBAL CALL_FUNCTION_EX
plain(**KWARGS) LOAD_GLOBAL LOAD_CONST BUILD_MAP LOAD_GLOBAL DICT_MERGE CALL_FUNCTION_EX
On the calling side the tuple goes in as it is: CALL_FUNCTION_EX receives
a ready object. Inside, a function with *args will build a tuple of its own —
g(*T) is T is False — but that happens on an ordinary call too. Before a
call with a dict, the
interpreter builds a new dict (BUILD_MAP) and copies the given one into it
(DICT_MERGE) — so the dict is copied on every call, before name matching even
begins. Hence the fourfold gap.
Passing by name works differently and costs less: it compiles not to
CALL_FUNCTION_EX but to CALL_KW, with a ready tuple of names among the
constants — no dict is built at all. Hence the gap: ×1.3–1.9 against the four
and more of plain(**kwargs). That is not an argument against keyword
arguments — readability is worth more than ten-odd nanoseconds (on 3.13.7 the gap came to 11.3 ns) — but it is an
argument against treating them as free inside a hot loop.
Wrappers stack
A decorator built on *args, **kwargs does both jobs at once: it receives with
stars (building a tuple and a dict) and passes with stars (taking them apart
again).
| layers | times dearer than a direct call |
|---|---|
| one | ×3.6–4.6 |
| three | ×8.5–11.9 |
At three layers, eight ninths of the time goes into getting to the work rather than doing it. This is not an argument against decorators — it is an argument for knowing where they are expensive: in a handler that hits a database those hundreds of nanoseconds are invisible; in a function called a million times per pass they become a visible share.
Version history
| Version | Change | What this means for your code |
|---|---|---|
| 3.0 | PEP 3102 introduces the bare * and the rule that everything after it is passed by name only. The motive is written down — a function with a variable number of arguments that also has "options"; before this they were dug out of **kwargs by hand. Hence co_kwonlyargcount, a field of its own on the code object. | |
| 3.8 | PEP 570 introduces /. Not for symmetry: while a parameter is passable by name, that name is taken and a key of the same name cannot reach **kwargs. Verified by running it: name_taken("a", name="b") is a TypeError, name_free("a", name="b") works. | |
| 3.12 | PEP 692 allowed Unpack[TypedDict] for **kwargs: a checker sees names and types again. Before it, every value had to share one type — “that behaviour can be very limiting”. The name Unpack itself arrived earlier, in 3.11 (PEP 646), so the annotation is accepted at an older target level too. | |
| 3.13 | The interpreter suggests the right name: “Did you mean 'timeout'?”. Verified on all four versions — absent on 3.11 and 3.12. One caveat that matters: it does not reach a function with **kwargs at all, because no error occurs there. |
How to answer in an interview
The short answer: a star in a declaration collects, a star in a call spreads.
One star collects the surplus positional arguments into the tuple args, two
collect the surplus keyword arguments into the dict kwargs; in a call, one
spreads a sequence into positional arguments and two spread a dict into keyword
arguments. Both objects are built even when there is nothing to pass: you get
() and {}, not None.
That is enough to answer correctly. What follows is what you add if the interviewer digs.
If the interviewer digs deeper
First, price, and the two stars do not share it: plain(*args) is almost free,
while plain(**kwargs) costs four times a direct call or more — consistently
across all four versions.
What separates a good answer: naming not the cost but the fact that a
**kwargs added "for flexibility" makes unknown keyword names acceptable. The call
connect("db", timeuot=5) stops being a TypeError and quietly lands in
options, while timeout stays equal to 30. Worse, since 3.13 the interpreter
can suggest the right name — and a function with **kwargs throws that help
away, behaving on 3.14 exactly as it did on 3.11.
Next they ask
The wrapper has @functools.wraps and inspect.signature shows the original signature. So the wrapper accepts exactly what the target accepts?
No. wraps hands the signature to tools, but it does not make the wrapper obey
it: the wrapper lets five positional arguments through, and any keyword too,
checking nothing. What fails is the call to the target inside — connect() takes from 1 to 3 positional arguments but 5 were given — and the traceback shows it:
one frame more than there are layers.
Why put a bare * or a / in the signature if the argument order can simply be agreed on?
A bare * forbids passing anything after it positionally: the order of
parameters stops being part of the contract and can be changed without breaking
callers. The slash does the opposite and matters where a function takes
**kwargs: while a parameter can be passed by name, that name is taken, and a
key of the same name cannot land in **kwargs. That is how dict itself is
declared — ($self, /, *args, **kwargs) — which is why dict(self="x") works.
Common misconceptions
*args is a list
A tuple. type(args).__name__ gives tuple on all four versions. It cannot be modified — and that is not pedantry: it is assembled afresh on every call, and mutability would mean someone could keep a reference and rewrite another call's arguments.
with no arguments, args and kwargs are None
An empty tuple and an empty dict. The language reference: *identifier “is initialized to a tuple… defaulting to the empty tuple”. So if args: works while if args is not None: is always true — and both look equally plausible.
the stars are just notation; there is no work behind them
There is work, and it differs between the two stars. plain(*args) is ×1.1–1.3 of a direct call, plain(**kwargs) is ×4.0–5.2. The bytecode shows why on all four versions: the tuple goes into the call as it is, while a call with a dict runs BUILD_MAP and DICT_MERGE first — the dict is copied on every call.
**kwargs adds flexibility and takes nothing away
It takes away the refusal on an unknown name: with two stars, unknown names become acceptable to the function. connect("db", timeuot=5) is a TypeError without **kwargs and a silently accepted call with it, leaving timeout equal to 30. And the hint “Did you mean 'timeout'?”, learned in 3.13, never fires either: there is no error for it to attach to.
functools.wraps fixes the wrapper's signature
It gives the signature back TO THE TOOLING without making it true. After @wraps, inspect.signature reports (host, port=5432, timeout=30), while the wrapper itself accepts five positional arguments and any keyword name — the exception comes from the inner function, and the traceback gains one frame per layer.
c and d in def f(a, *args, c, d=4) can be passed positionally
They are keyword-only, and not because of the defaults but because of position: everything after *args is passed by name only (PEP 3102). It is visible on the code object — co_kwonlyargcount is two. c has no default at all and is keyword-only regardless.
the slash in a signature is documentation decoration
Without it the parameter's name is taken for good: a key of the same name cannot reach **kwargs, and the call raises got multiple values for argument. That is exactly why dict is declared with a slash: its signature is ($self, /, *args, **kwargs), and dict(self="x") gives {'self': 'x'}. PEP 570's second argument: parameter names stop being part of the public contract and can be changed.
**kwargs cannot be annotated
Before 3.12 it could, but with one type for every value — PEP 692 calls that “very limiting”. From 3.12 there is Unpack[TypedDict], which gives the checker back names and types. It is the only automatic way to catch timeuot=5 before the code runs when **kwargs cannot be removed.
Practice
Two exercises. Answer first, then check against the real output: in both, the right answer comes from a recorded run rather than from an editor.
Practice · predict the output
def connect(dsn, timeout=30, **options):
return timeout, options
print(connect("db", timeout=5))
print(connect("db", timeuot=5))Practice · estimate
Knowledge check
What does probe() return for def probe(*args, **kwargs): return args, kwargs?
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- A star does opposite things on the two sides of a call. In a declaration it collects: one star gathers the surplus positional arguments into the tuple
args, two stars gather the surplus keyword arguments into the dictkwargs. In a call it spreads: one star lays a sequence out as separate positional arguments, two stars lay a dict out as separate keyword arguments. The collected objects arrive even when there is nothing to deliver: a call with no arguments produces()and{}inside, notNone. - Hence the main consequence: two stars added to a signature "for flexibility" make unknown keyword names acceptable to that function — there is now somewhere to put them. The call
connect("db", timeuot=5)stops being aTypeErrorand quietly lands inoptions, whiletimeoutstays equal to 30. The second consequence is price, and the two stars do not share it:plain(*args)is nearly free (×1.1–1.3 of a direct call),plain(**kwargs)costs four times and more (×4.0–5.2). Stable across all four versions; three decorator layers come to ×8.5–11.9, so eight ninths of the time goes into getting to the work rather than doing it. - Beyond that: numbers, versions and boundaries. The
()and{}are semantics, not a claim about allocation: the empty tuple in CPython is shared by everyone, and how much gets allocated is a question for the measurement below, not for the language. What becomes acceptable is exactly the unknown names; the rest of the binding rules stay where they were — a run ofbench/args-kwargs/binding_rules.pyputs each of them through a real call: strict(a=1, unknown=9) TypeError loose(a=1, unknown=9) a=1 b=2 kwargs={'unknown': 9} loose() TypeError ← required parameter loose(1, a=2) TypeError ← passed twice positional_only_loose(a=1, b=2, c=3) TypeError ← a name instead of a position- The last line is why the
/in a signature is not decoration. And the run prints the most surprising case separately: fordef f(a, /, b, *, c, **kwargs)the callf(1, b=2, c=3, a=99)is not an error —a=99goes intokwargs, because a positional-only parameter does not reserve its name for keyword arguments. - Worse: in 3.13 the interpreter learned to suggest the right name — "Did you mean 'timeout'?". A function with
**kwargsthrows that help away and behaves on 3.14 exactly as it did on 3.11. - The
/from PEP 570 is not decoration: while a parameter can be passed by name, that name is taken, and a key of the same name cannot reach**kwargs. This is howdictitself is declared: its signature is($self, /, *args, **kwargs).
In fact
- A tuple.
type(args).__name__givestupleon all four versions. It cannot be modified — and that is not pedantry: it is assembled afresh on every call, and mutability would mean someone could keep a reference and rewrite another call's arguments. - An empty tuple and an empty dict. The language reference:
*identifier“is initialized to a tuple… defaulting to the empty tuple”. Soif args:works whileif args is not None:is always true — and both look equally plausible. - There is work, and it differs between the two stars.
plain(*args)is ×1.1–1.3 of a direct call,plain(**kwargs)is ×4.0–5.2. The bytecode shows why on all four versions: the tuple goes into the call as it is, while a call with a dict runsBUILD_MAPandDICT_MERGEfirst — the dict is copied on every call. - It takes away the refusal on an unknown name: with two stars, unknown names become acceptable to the function.
connect("db", timeuot=5)is aTypeErrorwithout**kwargsand a silently accepted call with it, leavingtimeoutequal to 30. And the hint “Did you mean 'timeout'?”, learned in 3.13, never fires either: there is no error for it to attach to. - It gives the signature back TO THE TOOLING without making it true. After
@wraps,inspect.signaturereports(host, port=5432, timeout=30), while the wrapper itself accepts five positional arguments and any keyword name — the exception comes from the inner function, and the traceback gains one frame per layer. - They are keyword-only, and not because of the defaults but because of position: everything after
*argsis passed by name only (PEP 3102). It is visible on the code object —co_kwonlyargcountis two.chas no default at all and is keyword-only regardless. - Without it the parameter's name is taken for good: a key of the same name cannot reach
**kwargs, and the call raisesgot multiple values for argument. That is exactly whydictis declared with a slash: its signature is($self, /, *args, **kwargs), anddict(self="x")gives{'self': 'x'}. PEP 570's second argument: parameter names stop being part of the public contract and can be changed. - Before 3.12 it could, but with one type for every value — PEP 692 calls that “very limiting”. From 3.12 there is
Unpack[TypedDict], which gives the checker back names and types. It is the only automatic way to catchtimeuot=5before the code runs when**kwargscannot be removed.
By version
- 3.0
- PEP 3102 introduces the bare
*and the rule that everything after it is passed by name only. The motive is written down — a function with a variable number of arguments that also has "options"; before this they were dug out of**kwargsby hand. Henceco_kwonlyargcount, a field of its own on the code object.< - 3.8
- PEP 570 introduces
/. Not for symmetry: while a parameter is passable by name, that name is taken and a key of the same name cannot reach**kwargs. Verified by running it:name_taken("a", name="b")is aTypeError,name_free("a", name="b")works.< - 3.12
- PEP 692 allowed
Unpack[TypedDict]for**kwargs: a checker sees names and types again. Before it, every value had to share one type — “that behaviour can be very limiting”. The nameUnpackitself arrived earlier, in 3.11 (PEP 646), so the annotation is accepted at an older target level too.< - 3.13
- The interpreter suggests the right name: “
Did you mean 'timeout'?”. Verified on all four versions — absent on 3.11 and 3.12. One caveat that matters: it does not reach a function with**kwargsat all, because no error occurs there.<
What is covered
- Base: a star on the two sides of a call
- Mechanism 1: what these objects are
- Mechanism 2: the bug that never crashes
- Mechanism 3: `/` and the bare `*` — on taking names
- Mechanism 4: what a wrapper does to a signature
- Mechanism 5: typing — giving the erased names back
- Deeper: what the stars cost
- Version history
- How to answer in an interview
- Next they ask
- Common misconceptions
- Practice
- Knowledge check
Sources & further reading
6 SOURCES
- The language reference — function definitions and callsOfficial documentation. The grammar that fixes the order of the parts of a signature, and the rule this lesson verifies by running code: “If the form `*identifier` is present, it is initialized to a tuple receiving any excess positional parameters, defaulting to the empty tuple”. And for the dict: “If the form `**identifier` is present, it is initialized to a new ordered mapping receiving any excess keyword arguments, defaulting to a new empty mapping of the same type”. Verified: a call with no arguments delivers `()` and `{}`, not `None`.https://docs.python.org/3.14/reference/compound_stmts.html#function-definitions
- PEP 3102 — Keyword-Only ArgumentsPEP. Talin, Final, Python 3.0. The source of the bare `*` and of the rule that everything after it is passed by name only. The motive is stated outright: “One can easily envision a function which takes a variable number of arguments, but also takes one or more 'options' in the form of keyword arguments” — before this, those options had to be dug out of `**kwargs` by hand.https://peps.python.org/pep-3102/
- PEP 570 — Python Positional-Only ParametersPEP. Larry Hastings, Pablo Galindo Salgado, Mario Corchero, Eric N. Vander Weele; Final, Python 3.8. Two reasons for `/`, and the second is verified by running code in this lesson: “Without the ability to specify which parameters are positional-only, library authors must be careful when choosing appropriate parameter names” and “A key scenario is when a function accepts any keyword argument but can also accepts a positional one”. Verified by running it: `dict.__init__.__text_signature__` is `($self, /, *args, **kwargs)`, and the slash releases the name `self`, so `dict(self="x")` gives `{'self': 'x'}`.https://peps.python.org/pep-0570/
- PEP 692 — Using TypedDict for more precise **kwargs typingPEP. Franek Magiera, Final, Python 3.12. It names the limitation that made typing `**kwargs` nearly useless before: “Currently **kwargs can be type hinted as long as all of the keyword arguments specified by them are of the same type. However, that behaviour can be very limiting”. `Unpack[TypedDict]` is the only way to give a checker back the names and types that `**kwargs` erases.https://peps.python.org/pep-0692/
- What's New in Python 3.13 — improved error messagesOfficial documentation. A change that bears directly on this lesson: “The error message now tries to suggest the correct keyword argument when an incorrect keyword argument is passed to a function”, with the example `split() got an unexpected keyword argument 'max_split'. Did you mean 'maxsplit'?`. Verified on all four versions: absent on 3.11 and 3.12, present on 3.13 and 3.14 — and a function with `**kwargs` gets it on none of them.https://docs.python.org/3.14/whatsnew/3.13.html
- functools — wraps and update_wrapperOfficial documentation. The source of the behaviour that makes a wrapper's signature lie in two different ways depending on one line. Without `@wraps` the wrapper reports `(*args, **kwargs)`; with it, `inspect.signature` follows `__wrapped__` and reports the original function's signature — the one the wrapper does not obey. Both checked in
bench/args-kwargs/wrapper_signature.py.https://docs.python.org/3.14/library/functools.html#functools.wraps