Every Python developer has written a function. But far fewer developers have written a function that enforces how it gets called. That distinction matters more than it might seem at first glance.
Python gives you two special positional markers that you can drop right inside a function signature: the forward slash / and the asterisk *. Together, they let you carve a function's parameter list into up to three distinct zones, each with its own calling rules. Once you understand what these symbols actually do, you will find yourself reaching for them in production code, API design, library development, and anywhere else that interface clarity matters.
The Default Situation
Before diving into the special markers, it helps to understand what Python does by default. When you write a plain function, Python lets callers pass arguments either way they like:
def greet(name, message):
print(f"{message}, {name}!")
greet("Kandi", "Hello") # purely positional
greet(name="Kandi", message="Hello") # purely keyword
greet("Kandi", message="Hello") # mixed
greet(message="Hello", name="Kandi") # reversed keyword order
That flexibility is convenient for simple cases, but it can cause real problems when functions are part of a public API, when parameter names carry no semantic meaning, or when you need to rename a parameter in the future without breaking every caller. The / and * markers exist to impose structure where you need it.
The Function Signature Map
The Python documentation illustrates the concept with this signature:
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
...
Reading left to right, the zones work like this:
- pos1, pos2 appear before the
/. They are positional-only. Callers cannot use their names. - pos_or_kwd sits between
/and*. It can be passed either way. - kwd1, kwd2 appear after the
*. They are keyword-only. Callers must use their names.
Both / and * are optional. You can use one without the other, or skip both entirely. When you do use them, they give you precise control over the calling interface without changing anything about the function's internal logic.
Positional-Only Parameters: The / Marker
The / was formally introduced in Python 3.8 via PEP 570. Any parameter listed before the slash can only be passed by position. If a caller tries to use the parameter name, Python raises a TypeError.
def circle_area(radius, /):
import math
return math.pi * radius ** 2
circle_area(5) # works fine: 78.539...
circle_area(radius=5) # TypeError: circle_area() got some positional-only
# arguments passed as keyword arguments
Why positional-only parameters matter
1. The parameter name is an implementation detail, not a contract.
Consider a function that computes the distance between two points. Internally you might name the parameters x and y, but those names are arbitrary choices. If you rename them later, any caller using keyword arguments would break. With /, callers can never rely on the names in the first place.
def distance(x1, y1, x2, y2, /):
return ((x2 - x1)**2 + (y2 - y1)**2) ** 0.5
Now you can rename x1 to point_a_x in a future refactor and no external code breaks, because no external code was allowed to reference it by name.
2. Matching the behavior of built-in functions.
Many of Python's built-in functions only accept positional arguments. len(), abs(), print(), range() — these all enforce positional calling at the C implementation level. The / marker lets pure-Python functions match that same calling convention.
# This mirrors how many builtins work
def fast_parse(value, /):
return int(value)
3. Avoiding parameter name collisions with **kwargs.
When a function accepts arbitrary keyword arguments via **kwargs, a positional-only parameter name can appear in kwargs without conflict:
def build_query(table, /, **kwargs):
filters = " AND ".join(f"{k}={v!r}" for k, v in kwargs.items())
return f"SELECT * FROM {table} WHERE {filters}"
build_query("users", table="admin", active=True)
# Works. 'table' in kwargs refers to the kwarg, not the positional parameter.
Without the /, this would raise a TypeError because table would appear twice.
Keyword-Only Parameters: The * Marker
The * marker has been part of Python since version 3.0 (PEP 3102). Any parameter listed after a bare * in the signature must be passed by keyword. Positional calling is not allowed.
def send_email(recipient, *, subject, body):
print(f"To: {recipient}\nSubject: {subject}\nBody: {body}")
send_email("kandi@example.com", subject="Hello", body="Great article!") # works
send_email("kandi@example.com", "Hello", "Great article!") # TypeError
The bare * itself is not a parameter. It is simply a divider that tells Python: everything after this point requires a keyword. Note that *args works as the divider too, with the practical side effect of collecting extra positional arguments into a tuple.
def process(*args, verbose=False, strict=False):
for item in args:
if verbose:
print(f"Processing: {item}")
Here verbose and strict are keyword-only because they come after *args.
Why keyword-only parameters matter
1. Self-documenting call sites.
Keyword-only parameters force callers to name what they are passing. That makes call sites readable without opening the function definition.
def resize_image(path, /, *, width, height, keep_aspect_ratio=True):
...
# Which is clearer at the call site?
resize_image("photo.png", 1920, 1080, True) # hard to read
resize_image("photo.png", width=1920, height=1080, keep_aspect_ratio=True) # immediately clear
2. Boolean flags should almost always be keyword-only.
A positional boolean is nearly impossible to read at the call site. Keyword-only enforcement eliminates the ambiguity.
# Bad: what does True mean here?
connect("localhost", 5432, True, False)
# Good: intent is obvious
connect("localhost", 5432, use_ssl=True, retry=False)
3. Future-proofing with optional parameters.
If you later add new optional parameters, keyword-only parameters make it easy to insert them without disrupting positional argument order.
def export_data(data, /, *, format="csv", delimiter=",", encoding="utf-8"):
...
You can add a compression parameter later and it will not break any existing calls.
Combining Both Markers
The real power comes from using / and * together in the same signature. This is the pattern shown in the Python documentation diagram, and it is what library authors reach for when designing stable, readable APIs.
def create_user(username, email, /, *, role="viewer", active=True, send_welcome=False):
...
In this function:
- username and email are positional-only. They are required, order-dependent, and their names are irrelevant to callers.
- role, active, and send_welcome are keyword-only. They are optional, always named, and self-documenting at the call site.
create_user("kandi_b", "kandi@example.com", role="admin", send_welcome=True)
That call is clear, unambiguous, and resilient. Refactoring the parameter names on either side will not break anything.
Real-World Applications
Data processing pipelines
When writing ETL or data transformation functions, input data parameters are often positional (they change every call), while configuration options are keyword-only (they are tuned once and stay stable).
def normalize_records(records, schema, /, *, drop_nulls=True, lowercase_keys=False, strict_types=False):
result = []
for record in records:
if drop_nulls:
record = {k: v for k, v in record.items() if v is not None}
if lowercase_keys:
record = {k.lower(): v for k, v in record.items()}
result.append(record)
return result
Configuration and settings objects
When building functions that configure system components, keyword-only parameters act as a checklist. Callers cannot accidentally omit or swap values.
def configure_logging(app_name, /, *, level="INFO", log_to_file=False, filepath=None, max_bytes=10_000_000):
import logging
logger = logging.getLogger(app_name)
logger.setLevel(getattr(logging, level))
if log_to_file and filepath:
handler = logging.handlers.RotatingFileHandler(filepath, maxBytes=max_bytes)
logger.addHandler(handler)
return logger
Mathematical and scientific functions
When replicating the style of mathematical notation, positional-only parameters read naturally. There is no meaningful "keyword" for x and y in a geometric formula.
def lerp(a, b, t, /):
"""Linear interpolation between a and b at parameter t."""
return a + (b - a) * t
def clamp(value, min_val, max_val, /):
return max(min_val, min(max_val, value))
Library and framework design
Any Python library that cares about backward compatibility will eventually encounter the need for these markers. If you publish a function where callers can reference parameter names, changing those names is a breaking change. Positional-only marking eliminates that risk entirely.
# Public API function in a hypothetical library
def fetch(url, /, *, timeout=30, headers=None, follow_redirects=True, verify_ssl=True):
...
The URL is positional-only because the name adds no value. The options are keyword-only because they require deliberate, named choices.
Decorator factories
Decorator factories benefit from keyword-only parameters when wrapping functions with optional behavior flags.
def retry(func=None, /, *, max_attempts=3, delay=1.0, exceptions=(Exception,)):
import functools, time
def decorator(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return f(*args, **kwargs)
except exceptions:
if attempt < max_attempts - 1:
time.sleep(delay)
return f(*args, **kwargs)
return wrapper
if func is not None:
return decorator(func)
return decorator
@retry(max_attempts=5, delay=0.5)
def fetch_data(url):
...
Common Mistakes and Gotchas
Putting / after *
The slash must come before the asterisk. Reversing them is a SyntaxError.
def bad(a, *, b, /, c): # SyntaxError
pass
Keyword-only parameters without defaults are required. This is a design choice, not a bug, but it surprises developers who assume keyword-only always means optional.
def process(data, *, mode): # 'mode' is required AND keyword-only
...
process([1, 2, 3]) # TypeError: missing required keyword-only argument: 'mode'
process([1, 2, 3], mode="fast") # works
Confusing *args with the bare *
The bare * is a pure divider that accepts nothing. *args is a variadic collector. Both make everything after them keyword-only, but only *args gathers extra positional arguments.
def only_kwd(a, *, b): # b is keyword-only, no extra positionals allowed
pass
def flexible(a, *args, b): # b is keyword-only, extra positionals go into args
pass
Checking Parameter Kinds Programmatically
Python's inspect module exposes the kind of each parameter, which is useful for writing decorators, documentation generators, or validation tools.
import inspect
def example(pos_only, /, regular, *, kwd_only):
pass
sig = inspect.signature(example)
for name, param in sig.parameters.items():
print(f"{name}: {param.kind.name}")
Output:
pos_only: POSITIONAL_ONLY
regular: POSITIONAL_OR_KEYWORD
kwd_only: KEYWORD_ONLY
The four kinds you will encounter are POSITIONAL_ONLY, POSITIONAL_OR_KEYWORD, VAR_POSITIONAL (*args), and KEYWORD_ONLY. There is also VAR_KEYWORD for **kwargs.
When to Actually Use These
Not every function needs this treatment. Small internal helpers, one-off scripts, and quick utility functions rarely benefit from the added structure. The special markers shine in specific situations:
Use both markers together when designing public APIs, library interfaces, or any function signature that needs to be stable across versions while remaining readable at every call site.
- Use positional-only (
/) when the parameter name carries no meaning to callers, when you want freedom to rename parameters internally, or when a name collision with**kwargsis possible. - Use keyword-only (
*) when a function has boolean flags or options that would be ambiguous positionally, when you want call sites to be self-documenting, or when you anticipate adding more optional parameters over time. - Use both together when designing public APIs, library interfaces, or any function signature that needs to be stable across versions while remaining readable at every call site.
These are not obscure features for advanced Python wizards. They are practical tools that show up in the Python standard library, in popular third-party packages, and increasingly in well-maintained production codebases. Understanding what the diagram in the Python documentation is actually telling you puts you in a much better position to both read other people's code and write your own with intention.
The next time you reach for a default-argument function with four boolean parameters, consider whether those booleans should be keyword-only. The next time you write a math-style utility that takes x and y, consider whether those names should ever be callable by keyword at all. Once you start asking those questions, these two small symbols will earn a permanent spot in your toolkit.