Skip to main content

Keyword-Only and Positional-Only Parameters

Reading time: ~13 minutes | Level: Foundation → Engineering

Have you ever called a Python function and been confused about which argument goes where?

# Which argument is which? You have to check the docs.
sorted(my_list, None, True) # What does True mean here?

# This is much clearer - Python enforces it:
sorted(my_list, key=None, reverse=True) # Unambiguous

Python 3 introduced keyword-only parameters (enforced with *) and Python 3.8 added positional-only parameters (enforced with /). Together they let you design function signatures that communicate intent clearly and protect your API from breaking changes.

If you have ever looked at the signature of Python's built-in open(), sorted(), or range() and wondered what those * symbols mean - this is the page.

What You Will Learn

  • What keyword-only parameters are and how * enforces them
  • What positional-only parameters are and how / enforces them (Python 3.8+)
  • How to combine positional, positional-only, regular, keyword-only, and variadic in one signature
  • The exact TypeError messages these rules produce
  • How Python's built-in functions use these conventions
  • Why keyword-only parameters prevent entire categories of bugs
  • How positional-only parameters protect API stability
  • When to use each in your own function designs

Prerequisites

  • Python function syntax: def, parameters, default values
  • How to call functions with positional and keyword arguments
  • Basic understanding of *args and **kwargs (covered next in Topic 05)

Mental Model: The Signature Separator Rules

FULL PARAMETER SIGNATURE MAP
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

def func(pos_only, /, normal, *, kw_only):
───────── ────── ──────────────
│ │ │
│ │ └── After * : KEYWORD-ONLY
│ │ Must be called as func(kw_only=value)
│ │
│ └── Between / and * : REGULAR
│ Can be passed positionally OR as keyword

└── Before / : POSITIONAL-ONLY
Must be called as func(value)
Cannot use func(pos_only=value) - TypeError

RULE SUMMARY:
/ marks end of positional-only zone
* marks start of keyword-only zone
Between / and * : flexible (positional or keyword)
*args works the same as bare * for establishing keyword-only zone

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Watch: Python Function Parameters

Part 1 - Keyword-Only Parameters with *

A keyword-only parameter can only be passed by name, never by position. You create them by placing a bare * in the parameter list - everything after * must be passed as a keyword argument.

def create_user(name, email, *, admin=False, verified=False):
# ^
# bare * separates positional from keyword-only
return {
"name": name,
"email": email,
"admin": admin,
"verified": verified,
}

# Valid calls:
create_user("Alice", "[email protected]")
create_user("Alice", "[email protected]", admin=True)
create_user("Alice", "[email protected]", admin=True, verified=True)

# Invalid - TypeError:
create_user("Alice", "[email protected]", True)
# TypeError: create_user() takes 2 positional arguments but 3 were given

The TypeError message is the enforcement mechanism. Python refuses to let you pass admin or verified positionally.

Keyword-only without defaults (required keyword arguments)

Keyword-only parameters can also be required (no default):

def send_email(subject, body, *, to, from_addr):
# ^^ required keyword-only (no defaults)
print(f"From: {from_addr}")
print(f"To: {to}")
print(f"Subject: {subject}")
print(body)

# Must pass to and from_addr explicitly:
send_email("Hello", "Hi there", to="[email protected]", from_addr="[email protected]")

# Missing required keyword-only → TypeError:
send_email("Hello", "Hi there", to="[email protected]")
# TypeError: send_email() missing 1 required keyword-only argument: 'from_addr'

Part 2 - Why Keyword-Only Parameters Matter

Clarity at the call site

# WITHOUT keyword-only: what does True mean?
resize_image("photo.jpg", 800, 600, True, False, True)

# WITH keyword-only: unambiguous
resize_image("photo.jpg", 800, 600,
maintain_aspect=True,
overwrite=False,
strip_metadata=True)

Preventing boolean flag confusion

Boolean arguments are one of the most common API smells. Keyword-only enforcement forces callers to be explicit:

def deploy(app, environment, *, dry_run=False, verbose=False, force=False):
"""Deploy an application. All flags must be explicit."""
if dry_run:
print(f"[DRY RUN] Would deploy {app} to {environment}")
return
# ... actual deployment

No one can accidentally call deploy("myapp", "prod", True) and silently enable dry_run.

In Python's standard library

import inspect

# sorted() uses keyword-only for key and reverse
print(inspect.signature(sorted))
# (iterable, /, *, key=None, reverse=False)

# print() uses keyword-only for sep, end, file, flush
print(inspect.signature(print))
# (*args, sep=' ', end='\n', file=None, flush=False)

Part 3 - Positional-Only Parameters with / (Python 3.8+)

A positional-only parameter can only be passed by position - passing it as a keyword raises a TypeError. You create them by placing / in the parameter list - everything before / is positional-only.

def calculate(x, y, /):
# ^
# / marks end of positional-only zone
return x + y

# Valid:
calculate(3, 4) # 7

# Invalid - TypeError:
calculate(x=3, y=4)
# TypeError: calculate() got some positional-only arguments passed as keyword arguments: 'x, y'

Why positional-only parameters exist

API stability: If a parameter is positional-only, you can rename it in future versions without breaking callers.

# Original API - x and y are positional-only
def transform(x, y, /, scale=1.0):
return x * scale, y * scale

# Future version - renamed to px, py internally
def transform(px, py, /, scale=1.0):
return px * scale, py * scale

# Callers who wrote transform(10, 20) still work fine
# Nobody could have written transform(x=10, y=20) - it was never allowed

Performance: The CPython interpreter can use faster lookup for positional-only parameters since they cannot appear in **kwargs.

Built-in functions are positional-only

Many built-in functions use positional-only parameters for historical reasons (their C implementations don't have named parameters exposed to Python):

import inspect

print(inspect.signature(len))
# (obj, /)

print(inspect.signature(abs))
# (x, /)

print(inspect.signature(int))
# (x=0, /) or (x=0, base=10, /)

Part 4 - Combining All Parameter Kinds

Python allows all four kinds in one signature, in a strict order:

def f(pos_only_a, pos_only_b, /, regular_a, regular_b=10, *args, kw_only_a, kw_only_b=99, **kwargs):

The order rules:

  1. Positional-only parameters (before /)
  2. Regular parameters (between / and *)
  3. *args or bare *
  4. Keyword-only parameters (after *)
  5. **kwargs
def advanced(a, b, /, c, d=10, *, e, f=99):
print(f"a={a}, b={b}, c={c}, d={d}, e={e}, f={f}")

# Valid:
advanced(1, 2, 3, e=5) # a=1, b=2, c=3, d=10, e=5, f=99
advanced(1, 2, 3, 4, e=5, f=6) # a=1, b=2, c=3, d=4, e=5, f=6
advanced(1, 2, c=3, e=5) # a=1, b=2, c=3 (c is regular, can be keyword)

# Invalid:
advanced(a=1, b=2, c=3, e=5) # TypeError: a and b are positional-only
advanced(1, 2, 3) # TypeError: e is required keyword-only

Real-world example: a training function

def train_model(
model, # positional-only (implementation detail)
dataset, # positional-only
/,
epochs=10, # regular (flexible)
batch_size=32, # regular (flexible)
*,
learning_rate, # keyword-only, required - important, must be explicit
optimizer="adam", # keyword-only - algorithmic choice, must be named
verbose=True, # keyword-only - behavioral flag
):
print(f"Training for {epochs} epochs, lr={learning_rate}")

The caller must name learning_rate, making accidental misuse of the API impossible.

Part 5 - Error Messages and Enforcement

Understanding what errors these rules produce helps you diagnose issues quickly:

def strict_func(pos_only, /, regular, *, kw_only):
pass

# Passing positional-only as keyword:
strict_func(pos_only=1, regular=2, kw_only=3)
# TypeError: strict_func() got some positional-only arguments passed
# as keyword arguments: 'pos_only'

# Passing keyword-only positionally:
strict_func(1, 2, 3)
# TypeError: strict_func() takes 2 positional arguments but 3 were given

# Missing required keyword-only:
strict_func(1, 2)
# TypeError: strict_func() missing 1 required keyword-only argument: 'kw_only'

# Too many positional:
strict_func(1, 2, 3, kw_only=4)
# TypeError: strict_func() takes 2 positional arguments but 3 were given

AI/ML Real-World Connection

Keyword-only parameters are used extensively in PyTorch and scikit-learn to make model APIs explicit and safe.

# PyTorch: torch.nn.functional uses keyword-only for critical parameters
import torch
import torch.nn.functional as F

# Dropout requires p= to be explicit
output = F.dropout(input, p=0.5, training=True, inplace=False)

# Without keyword-only enforcement, you could accidentally write:
# output = F.dropout(input, 0.5, True, False)
# - which arguments are which? Keyword-only forces clarity.
# scikit-learn style: explicit keyword-only for model parameters
from sklearn.ensemble import RandomForestClassifier

# These keyword-only-style arguments must be named:
clf = RandomForestClassifier(
n_estimators=100,
max_depth=5,
random_state=42,
n_jobs=-1,
)
# You would never write RandomForestClassifier(100, 5, None, 42, ...)
# The keyword-only convention makes it self-documenting.
# Design your own ML utility with keyword-only:
def evaluate_model(model, X_test, y_test, /, *, metrics, verbose=True, threshold=0.5):
"""
Evaluate model performance.

model, X_test, y_test are positional-only (implementation details).
metrics must be named - prevents accidentally passing wrong list.
"""
results = {}
predictions = model.predict(X_test)

for metric_name in metrics:
if metric_name == "accuracy":
from sklearn.metrics import accuracy_score
results["accuracy"] = accuracy_score(y_test, predictions)

if verbose:
for name, value in results.items():
print(f"{name}: {value:.4f}")

return results

# Caller must be explicit about metrics:
evaluate_model(clf, X_test, y_test, metrics=["accuracy"])

Common Mistakes

Mistake 1: Forgetting * makes subsequent parameters keyword-only

def func(a, b, *, c, d):
pass

# Common mistake - thinking c and d can be positional:
func(1, 2, 3, 4)
# TypeError: func() takes 2 positional arguments but 4 were given

# Correct:
func(1, 2, c=3, d=4)

Mistake 2: Putting / in the wrong position

# Syntax error - / must come before *
def bad(a, *, b, /, c): # SyntaxError: invalid syntax
pass

# Correct order: positional-only / regular * keyword-only
def good(a, /, b, *, c):
pass

Mistake 3: Missing the difference between bare * and *args

# Bare * : marks keyword-only boundary, collects nothing
def f(a, *, b):
print(a, b) # no *args collected

f(1, b=2) # works
f(1, 2, b=3) # TypeError - extra positional not collected

# *args : collects extra positionals AND marks keyword-only boundary
def g(a, *args, b):
print(a, args, b)

g(1, 2, 3, b=4) # a=1, args=(2, 3), b=4

Interview Questions

Q1: What does placing * (bare asterisk) in a function signature do?

Answer: A bare * acts as a separator - all parameters after it must be passed as keyword arguments. It does not collect any arguments into a tuple (unlike *args). Example: def f(a, *, b) - b must be passed as f(1, b=2), not f(1, 2).

Q2: What does / in a function signature do, and when was it added?

Answer: / was added in Python 3.8. Parameters before / are positional-only - they cannot be passed as keyword arguments. This allows renaming internal parameter names without breaking callers, and matches how many C extension functions work. Example: len(obj) not len(obj=mylist).

Q3: Why are keyword-only parameters useful for API design?

Answer: They eliminate ambiguity at call sites, especially for boolean flags and optional configuration. They force callers to be explicit about which argument is which, making code more readable and resistant to argument-order mistakes. They also allow adding new keyword-only parameters later without breaking existing call signatures.

Q4: What is the correct parameter order in a Python function signature?

Answer: The order must be: positional-only (before /), regular (between / and *), *args or bare *, keyword-only (after *), **kwargs. Violating this order raises a SyntaxError.

Q5: How are Python's built-in functions like sorted() and len() defined with these separators?

Answer: sorted(iterable, /, *, key=None, reverse=False) - iterable is positional-only and key/reverse are keyword-only. len(obj, /) - obj is positional-only. You can inspect this with inspect.signature(sorted).

Quick Reference Cheatsheet

SyntaxWhat it meansExample
def f(a, b)Both regular (flexible)f(1, 2) or f(a=1, b=2)
def f(a, /, b)a positional-onlyf(1, 2) only; f(a=1, b=2) → TypeError
def f(a, *, b)b keyword-onlyf(1, b=2) only; f(1, 2) → TypeError
def f(a, /, b, *, c)a pos-only, c kw-onlyf(1, 2, c=3)
def f(a, *args, b)b keyword-only, args collects extrasf(1, 2, 3, b=4)
inspect.signature(f)Inspect full signatureShows / and * separators

Graded Practice Challenges

Level 1 - True or False

True or false: In def f(a, *, b=10), calling f(1, 2) is valid.

Show Answer

False.

The * makes b keyword-only. f(1, 2) tries to pass 2 as a positional argument after the *, which is not allowed. The correct call is f(1) (using the default b=10) or f(1, b=2).

Level 2 - Debug the Code

def send_notification(user_id, message, urgent=False, channel="email"):
pass

# A new developer joins and calls:
send_notification(42, "Server down!", True, "sms")

What is the problem and how would you redesign the signature to prevent it?

Show Answer

Problem: urgent=True and channel="sms" are passed positionally - the call is valid Python but True and "sms" are invisible at the call site. A reader has to check the signature to understand what True and "sms" mean. This is an API clarity problem.

Better design:

def send_notification(user_id, message, /, *, urgent=False, channel="email"):
pass

# Now the only valid calls are:
send_notification(42, "Server down!", urgent=True, channel="sms")
# The flags are always explicit and self-documenting

user_id and message become positional-only (they are unambiguous), and urgent/channel become keyword-only (they are optional flags that should be explicit).

Level 3 - Design Challenge

Design a log_metric function for a machine learning experiment tracker with these requirements:

  • name and value must be positional (they are always the first two things you pass)
  • step is a regular parameter with a default of None
  • tags, run_id, and timestamp must always be passed as keyword arguments
  • tags should default to an empty list (use None sentinel)
Show Reference Solution
from datetime import datetime
from typing import Optional

def log_metric(
name: str,
value: float,
step: Optional[int] = None,
*,
run_id: str, # keyword-only, required
tags: Optional[list] = None, # keyword-only with None sentinel
timestamp: Optional[datetime] = None, # keyword-only with None sentinel
) -> None:
"""
Log a metric value for a training run.

Args:
name: Metric name (e.g., "loss", "accuracy")
value: Metric value
step: Training step (epoch or batch number)
run_id: Experiment run identifier (must be explicit)
tags: Optional list of tags to group metrics
timestamp: Optional timestamp; defaults to current time
"""
if tags is None:
tags = []
if timestamp is None:
timestamp = datetime.now()

record = {
"name": name,
"value": value,
"step": step,
"run_id": run_id,
"tags": tags,
"timestamp": timestamp.isoformat(),
}
print(f"Logging metric: {record}")


# Usage - run_id must always be explicit:
log_metric("loss", 0.42, 100, run_id="exp-001")
log_metric("accuracy", 0.95, 100, run_id="exp-001", tags=["validation"])

# This is now impossible (enforced by keyword-only):
# log_metric("loss", 0.42, 100, "exp-001") → TypeError

Key Takeaways

  • A bare * in a signature makes all following parameters keyword-only - they must be named at the call site
  • A / in a signature (Python 3.8+) makes all preceding parameters positional-only - they cannot be named at the call site
  • Keyword-only parameters prevent argument-order bugs with boolean flags and optional configuration
  • Positional-only parameters protect API stability - internal parameter names can change without breaking callers
  • Python's built-ins (sorted, len, print) use these conventions extensively
  • The full parameter order is: positional-only / regular * keyword-only **kwargs
  • *args also marks the keyword-only boundary while collecting variadic positionals
  • Use inspect.signature(func) to see the full parameter specification of any function
© 2026 EngineersOfAI. All rights reserved.