Lesson 1 of 5
Parameters & Arguments
"The purpose of abstracting is not to be vague, but to create a new semantic level in which one can be absolutely precise." — Edsger W. Dijkstra, "The Humble Programmer" (EWD 340), 1972 ACM Turing Award Lecture
A function is the smallest unit of abstraction Python offers, and its signature is the contract. Everything a caller is allowed to do, everything they must supply, everything they may leave out — all of it is declared in one line, and that line is read far more often than the body beneath it.
Python's parameter system is unusually expressive as a result. Most languages give you positional arguments and, if you are lucky, default values. Python gives you positional arguments, keyword arguments, defaults, variadic collection of both kinds, and — since version 3.8 — the ability to declare that certain parameters may only be passed positionally while others may only be passed by name. That is a lot of vocabulary for one line of code, and it exists because the signature is where a function says what it means.
The cost of that expressiveness is that the rules interact. Which arguments can be omitted, which can be reordered, which can be named, and what happens when a caller gets it wrong are all consequences of where the parameters sit relative to two markers most Python programmers have seen and few can explain. This chapter covers the whole system, in the order the interpreter cares about.
We start with terminology, because the words parameter and argument are used interchangeably in conversation and mean different things precisely — and the distinction is what makes the rest of the rules describable at all.
Given def area(width, height), you may call area(3, 4), area(width=3, height=4), or area(3, height=4) — but area(width=3, 4) is a syntax error. Meanwhile a built-in like len refuses names entirely: len(obj=[1]) raises TypeError. Three different rules about how the same argument may be passed. Where do they come from?
Parameters are in the definition; arguments are at the call
A parameter is a name in the function definition — part of the contract. An argument is a value supplied at the call site. def area(width, height) declares two parameters; area(3, 4) passes two arguments. Keeping the words apart makes every later rule stateable: parameters have kinds, arguments have forms, and the errors happen when a form does not fit a kind.
There are two forms of argument. A positional argument is matched by its place in the call, and a keyword argument is matched by name. The same parameter can usually accept either form, which is why area(3, 4) and area(width=3, height=4) both work. The one hard rule at the call site is that positional arguments must all come before keyword arguments — that is what makes area(width=3, 4) a syntax error rather than a runtime one, because the parser cannot make sense of a position that follows a name.
Click Run to execute the code.
Two of those error messages are worth reading closely, because Python's argument errors are unusually specific and most people never look. got multiple values for argument 'width' means a value arrived positionally and by name for the same parameter — the position filled width, then the keyword tried to fill it again. missing 1 required positional argument: 'height' names the parameter, so the fix needs no investigation.
The len result is the first hint of the machinery behind the big question. len does not merely prefer positional arguments; it forbids names. That is a parameter kind, not a call-site rule, and Python 3.8 gave ordinary functions the same ability.
The slash marks the end of positional-only parameters
A parameter written before a bare / in the signature is positional-only: it can be filled by position and by nothing else. Passing it by name is a TypeError, and — this is the part that surprises people — its name is then free to be reused as a keyword argument for something else entirely.
The syntax arrived in Python 3.8 through PEP 570, but the behaviour is much older. Built-ins written in C have always been positional-only, which is why len(obj=[1, 2]) fails. Before 3.8 there was no way to say the same thing in pure Python, so the standard library documented it in prose — / was already used in the docs to mark the boundary long before it was legal syntax.
Click Run to execute the code.
That last example is the strongest practical argument for /. The function has a parameter called name, and the caller also wants to pass an HTML attribute called name. Without the slash the two collide and you get got multiple values for argument 'name'. With it, the parameter name is a private implementation detail and name="email" flows into attrs where the caller intended it. Any function that takes **kwargs on behalf of a caller — a wrapper, a builder, a proxy — wants its own parameters positional-only for exactly this reason.
The second argument for / is about promises you did not mean to make. Every parameter name that a caller is allowed to type becomes part of your public API. Rename target to text in a normal signature and you break everyone who wrote target=...; rename it behind a slash and nobody notices, because nobody could have written it.
PEP 457 (2013) introduced / as notation — a way for the documentation generator to describe C functions that Python could not yet express. For six years the standard library shipped signatures nobody could write. PEP 570 (2018, landing in Python 3.8) closed the gap by making the notation real syntax, and its rationale is unusually candid: the goal was to let pure-Python code be equivalent to C code, so that a function could be rewritten from one to the other without silently changing its contract.
The bare star marks the start of keyword-only parameters
The mirror image of / is a bare * in the parameter list. Everything after it is keyword-only: it can never be filled by position, only by name. There is no way to pass it accidentally, and no way to pass it in the wrong order.
Keyword-only parameters exist to stop a specific kind of unreadable call — the trailing run of bare booleans and magic numbers whose meaning is invisible at the call site. connect("db.internal", 5432, True, False, 30) tells the reader nothing. Force the tail of that signature to be keyword-only and the language stops the ambiguous call from compiling in the first place.
Click Run to execute the code.
Read the two error messages side by side, because they describe opposite failures. got some positional-only arguments passed as keyword arguments: 'a, b' is a caller who used names where names are banned. takes 4 positional arguments but 6 were given is a caller who used positions where positions run out — Python counts only the four slots that can be filled positionally, so the message reports 4, not 6. Neither message mentions the marker that caused it, which is why signatures like pk need to be read carefully before the traceback makes sense.
Note that * and *args occupy the same slot in the grammar. If a function already collects extra positional arguments with *args, every parameter after it is automatically keyword-only — the bare * is simply the version that collects nothing. That is the whole rule: the star, in either form, is the boundary past which positions cannot reach.
Think of a signature as a form at a counter. The boxes before the slash are unlabelled — the clerk fills them in order and the applicant never sees their names. The boxes in the middle have labels, and you may either fill them top-to-bottom or write in the labels yourself. The boxes after the star have only labels and no order at all; hand the clerk a loose value with no label and it will be refused, because there is nowhere for it to go.
The interpreter's matching order
When a call happens, Python matches arguments to parameters in a fixed sequence, and knowing that sequence explains every error message in this chapter. Positional arguments are assigned left to right into the positional slots. Keyword arguments are then matched by name into whatever remains. Any leftover positional arguments go to *args if it exists and are an error if it does not. Any leftover keywords go to **kwargs if it exists and are an error if they do not. Finally, parameters still unfilled take their defaults — and any that have no default raise missing required argument.
You do not have to trace that by hand. The inspect module exposes the same machinery Python itself uses, and Signature.bind performs the match without calling the function — the single most useful debugging tool for argument problems.
Click Run to execute the code.
Three things in that output repay attention. p.kind.description prints the parameter kind in plain English — positional-only, positional or keyword, keyword-only — so you never have to infer a kind from marker placement. bound.arguments before apply_defaults() contains only what the caller actually supplied, which is how you distinguish "the caller passed the default value explicitly" from "the caller passed nothing". And bound.args / bound.kwargs reconstruct a valid call, which is the standard way a decorator or proxy forwards arguments it has inspected.
A logging wrapper on an internal client kept recording timeout=30 for every request, and an engineer spent a morning hunting a config bug that did not exist. The wrapper called apply_defaults() before logging. Every unset parameter was being filled in with its default and then recorded as though the caller had asked for it. Removing one line made the log honest: what the caller passed, and nothing else.
Signatures as public contracts
Everything above is mechanism. The design question is which kind each parameter should be, and there is a usable default rule: make a parameter positional-only if its name is meaningless to the caller, and keyword-only if its value is meaningless without the name.
replace(target, old, new, /) passes the first test — nobody reading replace(s, "-", "+") wonders which is which, and the names target, old, new carry no information the order does not. connect(..., *, secure=True) passes the second — False alone is unreadable, secure=False is self-documenting. Parameters that fail both tests stay in the flexible middle, which is the majority of ordinary code and a perfectly good place to be.
The rule also has a maintenance dimension worth stating plainly: a keyword-only parameter can be added to a released function without breaking a single caller, because no existing call could have been relying on that position. A new positional parameter in the middle of a signature breaks everyone. This is why mature libraries accumulate long keyword-only tails and why * often appears in a signature long before any of the parameters after it were written.
Click Run to execute the code.
Notice that every call to render_report is readable without the definition open. That is the actual payoff, and it is not a stylistic preference — it is the difference between a reviewer being able to check a call and having to go look it up.
Look at dict.get, str.split, and sorted in the standard library and you will find all three kinds in use for exactly the reasons above. sorted(iterable, /, *, key=None, reverse=False) puts the data positionally where the name would add nothing, and forces the two options to be named because sorted(xs, None, True) would be unreadable. The signature is a design document, and it was chosen — the C implementation could have accepted anything.
Common Mistakes
Confusing a call-site rule with a parameter kind. "Positional before keyword" is a rule about how a call is written and produces a
SyntaxError. "This parameter is positional-only" is a property of the definition and produces aTypeError. Different failure, different fix.Reading
takes 4 positional arguments but 6 were givenas a miscount. Python counts the slots that can be filled positionally, not the total number of parameters. A signature with two keyword-only parameters reports the smaller number, and it is correct.Adding a new parameter in the middle of a released signature. Every caller who passed the following arguments positionally now passes them into the wrong slot — usually with no error, just wrong results. New options go after a
*.Taking
**kwargswithout making your own parameters positional-only. The moment a caller wants to pass a key that collides with one of your parameter names, they getgot multiple values for argumentand no way to work around it. A/costs one character and removes the whole class of bug.Assuming keyword arguments are slower enough to matter. They are marginally slower to bind, and in almost all real code the difference is invisible next to what the function body does. Choosing positional for speed in application code is optimising the wrong thing.
Treating parameter names as private when they are not. In an ordinary signature every name is public API. Renaming
datatorowsis a breaking change for anyone who wrotedata=..., whether or not you documented it.Using a bare
*and then never adding keyword-only parameters after it. A trailing*with nothing following it is aSyntaxError—named arguments must follow bare *. The marker must actually mark something.Expecting
*argsand a bare*to coexist. They occupy the same grammatical slot; a signature cannot have both. If you already have*args, everything after it is keyword-only automatically.Debugging argument mismatches by calling the function.
inspect.signature(f).bind(...)reproduces the exactTypeErrorwithout executing the body — no side effects, no half-written files, no network call, and a clean message about which parameter failed.Calling
apply_defaults()when you wanted to know what the caller passed. After that call,bound.argumentscontains every parameter, defaults included, and the distinction between "omitted" and "explicitly set to the default" is gone for good.
Interview Corner
Signature questions are popular because they are quick to ask and hard to bluff: either you know why / exists or you do not. The strongest answers connect syntax to consequence — not "the slash makes parameters positional-only" but "the slash keeps my parameter names out of my public API, which is what lets me take **kwargs safely."
f(x=1, 2) is a SyntaxError. Keyword arguments among themselves have no required order, so f(b=2, a=1) is fine.* in a signature? Give a case where it prevents a real bug.connect(host, port, False, True, 5) is unreadable and easy to transpose, and a transposition is silent. With *, the caller must write secure=False. It also makes the signature extensible — a new keyword-only parameter cannot break existing callers, whereas a new positional one shifts everything after it.**kwargs and forwards them. What can go wrong, and how does PEP 570 fix it?got multiple values for argument, and the caller has no workaround short of not using that key. Marking my parameters positional-only with / removes their names from the caller's namespace entirely, so a collision is impossible. The same change also frees me to rename them later without a breaking release, because no caller could ever have referred to them by name. That is the real point of PEP 570: it lets a pure-Python function make the same narrow promise a C function always could.Write describe(func), which takes any function and returns a list of "name: kind" strings using inspect.signature — then use it to check your own guesses about three signatures before you run it. Extend it with can_call(func, *args, **kwargs), which returns True if the arguments would bind and the error message if they would not, without ever calling func.
Click Run to execute the code.
Summary
- Parameter versus argument. A parameter is a name in the definition; an argument is a value at the call. Kinds belong to parameters, forms belong to arguments, and every error is a form that does not fit a kind.
- Two argument forms. Positional arguments match by place, keyword arguments match by name, and at the call site all positions must precede all names — a
SyntaxErrorif violated. - The slash makes parameters positional-only. Names before
/cannot be used by the caller, which keeps them out of your public API and out of the way of**kwargs. - The star makes parameters keyword-only. Nothing after a bare
*can be filled by position, which kills the unreadable trailing-flag call and makes the signature safely extensible. *argsand a bare*are the same slot. A signature can have one or the other, never both, and everything after either one is keyword-only.- Matching order is fixed. Positions left to right, then keywords by name, then leftovers into
*args/**kwargs, then defaults, thenmissing required argumentfor whatever is still empty. - The error messages are precise.
got multiple values for argumentmeans one parameter was filled twice;takes N positional argumentscounts only positionally fillable slots, not total parameters. inspect.signatureis the ground truth..parameters[...].kind.descriptionnames each kind in English and.bind(...)reproduces a call's exactTypeErrorwithout running the body.bound.argumentsbeforeapply_defaults()is what the caller actually passed. Applying defaults first destroys the distinction between omitted and explicitly-default.- Design rule. Positional-only when the name adds nothing; keyword-only when the value means nothing without the name; flexible in between, which is most code.
The next chapter takes the one piece of a signature this one deliberately skipped — the default value — and asks a question with a famous answer: when exactly is def f(items=[]) evaluated, and what does it mean that the answer is "once"?
A signature is the only part of a function that other people are forced to read. The body can be rewritten, optimised, or replaced wholesale and nobody outside needs to know — but every name and every marker in that first line is a promise, and promises are much harder to take back than code. Writing the signature last, once the body has told you what the function really needs, tends to produce a better contract than writing it first and living with the guess.
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.