Positional vs keyword arguments
Overview
Python lets you enforce HOW arguments must be passed using two special markers in the signature: * (bare star) makes everything after it keyword-only — callers MUST name those arguments: def create_user(name, *, role='member', active=True): ... create_user('alice', role='admin') # ok create_user('alice', 'admin') # TypeError — role must be named / makes everything before it positional-only — callers CANNOT name those arguments: def build_path(base, endpoint, /): return base + endpoint build_path('/api', '/users') # ok build_path(base='/api', endpoint='/users') # TypeError Both can appear together: def f(pos_only, /, normal, *, kw_only). / requires Python 3.8+. Think of * as a 'name required from here on' wall, and / as a 'no naming allowed before this' wall.
Keyword-only parameters make call sites self-documenting (create_user('alice', role='admin') is clearer than create_user('alice', 'admin')) and prevent callers from passing booleans in the wrong order. Positional-only parameters let library authors rename internal parameters without breaking callers.
Where used: FastAPI Depends() uses keyword-only injection, Python stdlib (sorted(key=..., reverse=...)), Pydantic model constructors
Why learn this
- Writing functions where boolean flags must be named (e.g. send_email(..., *, dry_run=False)) to prevent silent argument-order bugs
- Reading Python 3.8+ library APIs that use / in their signatures
Code walkthrough
def resize(width, height, /, *, quality='high'):
print(f'{width}x{height} at {quality}')
resize(1920, 1080)
resize(1280, 720, quality='low')
Focus: width and height are positional-only (before /); quality is keyword-only (after *) — the call site must name quality but cannot name width or height.
Common mistakes
- Passing a keyword-only argument positionally: If a parameter follows * in the signature, you must name it. This means you must pass it as a keyword argument (where the parameter name is the keyword). Passing it as a positional argument raises TypeError.
- Naming a positional-only argument: If a parameter precedes / in the signature, you cannot pass it by name. Doing so raises TypeError.
- Confusing * (keyword-only marker) with *args: A bare * in the parameter list is just a separator — it collects nothing. *args is a named collector for extra positional arguments. They look similar but behave completely differently.
Glossary
- signature
- The definition of a function that shows its name and exactly what arguments it expects to receive. Example: `def add(a, b):`.
- call sites
- The specific places in your code where a function is actually executed or invoked. Example: `result = my_func(5)` is a call site.
- keyword argument
- Passing a value to a function by explicitly writing the parameter's name (the keyword), followed by an equals sign, and then the value. Example: `role='admin'`.
- positional argument
- Passing a value to a function purely based on its order in the list, without writing any parameter name. Example: in `create_user('alice')`, `'alice'` is a positional argument because it's first.
- keyword-only
- A parameter that MUST be passed as a keyword argument, meaning you are forced to type out its name. Example: `def f(*, kw_only=True):`.
- positional-only
- A parameter that MUST be passed as a positional argument, meaning you are forbidden from typing out its name. Example: `def f(pos_only, /):`.
- TypeError
- An error that Python throws when an operation or function is applied to an object of inappropriate type, or when function arguments don't match the signature. Example: `len(5)` raises a `TypeError`.
Recall questions
- What does a bare * in a function signature enforce?
- What does / in a function signature enforce?
- What practical benefit does using * to require keyword arguments provide?
Understanding checks
What is the output of this code?
localhost:8080 (False)
`host` is positional-only (passed as "localhost"), `port` can be passed normally (passed as a keyword argument here), and `secure` has a default value, so it doesn't need to be provided.
Why does this function call raise a TypeError?
Because `url` is a positional-only parameter, but it is being passed as a keyword argument.
The `/` in the signature enforces that all parameters before it (like `url`) must be passed purely by their position, not by name.
Practice tasks
Refactor to Enforce Keyword and Positional Arguments
The function below currently allows any argument to be passed positionally or by name. Modify the function signature so that `user_id` and `message` MUST be passed positionally, and `dry_run` and `urgent` MUST be passed by keyword.
Challenge
Safe cache setter
Write set_cache(key, value, /, *, ttl=60, overwrite=False) where key and value are positional-only (internal names may change) and ttl and overwrite are keyword-only flags. Print a summary line for each call. Demonstrate three calls: default, custom ttl, and overwrite=True.
Continue learning
Previous: *args & **kwargs