Functions & Functional Python

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.

The big question

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.

Python — Try it Yourself
Loading editor…
Output
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.

Python — Try it Yourself
Loading editor…
Output
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.

A little history

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.

Python — Try it Yourself
Loading editor…
Output
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.

Everyday analogy

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.

Python — Try it Yourself
Loading editor…
Output
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.

The engineer's notebook

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.

Python — Try it Yourself
Loading editor…
Output
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.

In the real world

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

  1. 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 a TypeError. Different failure, different fix.

  2. Reading takes 4 positional arguments but 6 were given as 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.

  3. 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 *.

  4. Taking **kwargs without making your own parameters positional-only. The moment a caller wants to pass a key that collides with one of your parameter names, they get got multiple values for argument and no way to work around it. A / costs one character and removes the whole class of bug.

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

  6. Treating parameter names as private when they are not. In an ordinary signature every name is public API. Renaming data to rows is a breaking change for anyone who wrote data=..., whether or not you documented it.

  7. Using a bare * and then never adding keyword-only parameters after it. A trailing * with nothing following it is a SyntaxError — named arguments must follow bare *. The marker must actually mark something.

  8. Expecting *args and 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.

  9. Debugging argument mismatches by calling the function. inspect.signature(f).bind(...) reproduces the exact TypeError without executing the body — no side effects, no half-written files, no network call, and a clean message about which parameter failed.

  10. Calling apply_defaults() when you wanted to know what the caller passed. After that call, bound.arguments contains 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."

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Junior
What is the difference between a positional argument and a keyword argument, and what is the one ordering rule at the call site?
Model answer — A positional argument is matched by its place in the call, a keyword argument by name. At the call site all positional arguments must come before all keyword arguments — f(x=1, 2) is a SyntaxError. Keyword arguments among themselves have no required order, so f(b=2, a=1) is fine.
Mid
Why would you write a bare * in a signature? Give a case where it prevents a real bug.
Model answer — It makes every following parameter keyword-only, so options cannot be passed as bare positional values. The classic case is a tail of flags: 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.
Senior
You maintain a widely used library function that accepts **kwargs and forwards them. What can go wrong, and how does PEP 570 fix it?
Model answer — Any caller key that collides with one of my own parameter names raises 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.
Company lens — how the same topic gets asked
micro1Practical: positional, keyword and default parameters in one signature.
GoogleDepth: the interpreter's matching order, and what / and * enforce.
MetaDesign: a signature as a public contract you cannot cheaply change.
AmazonReality: dict.get, str.split and sorted, and why their shapes differ.
Hands-on

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.

Python — Try it Yourself
Loading editor…
Output
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 SyntaxError if 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.
  • *args and 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, then missing required argument for whatever is still empty.
  • The error messages are precise. got multiple values for argument means one parameter was filled twice; takes N positional arguments counts only positionally fillable slots, not total parameters.
  • inspect.signature is the ground truth. .parameters[...].kind.description names each kind in English and .bind(...) reproduces a call's exact TypeError without running the body.
  • bound.arguments before apply_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"?

Editorial reflection

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.