I once inherited a codebase with a function signature like def create_report(data, True, False, None, 5). I’m exaggerating slightly for effect, but only slightly — it was genuinely a function called with five positional arguments, several of them booleans, and I had absolutely no idea what any of them meant without opening the function definition. That experience made me a firm believer in forcing keyword arguments wherever a call site could otherwise become an unreadable wall of positional values. This guide covers everything I know about keyword-only arguments in Python: how to enforce them, why they matter, and where I use them deliberately.
The Problem: Positional Arguments Are Ambiguous
Consider this function:
def create_user(name, email, is_admin, send_welcome_email):
print(f"{name}, {email}, admin={is_admin}, welcome_email={send_welcome_email}")
create_user("Ali", "ali@example.com", True, False)
Reading that call site, I have no idea what True and False mean without checking the function signature. Swap the order of is_admin and send_welcome_email by mistake, and the bug is silent — both are booleans, so Python won’t complain, and the function will just misbehave quietly.
The Solution: Keyword-Only Arguments With a Bare *
Python lets you force certain parameters to be passed by keyword only, using a bare * in the function signature. Everything after that * cannot be passed positionally.
def create_user(name, email, *, is_admin, send_welcome_email):
print(f"{name}, {email}, admin={is_admin}, welcome_email={send_welcome_email}")
create_user("Ali", "ali@example.com", is_admin=True, send_welcome_email=False)
Now the call site is unambiguous, and trying to pass them positionally raises an immediate, loud error instead of a silent logic bug:
create_user("Ali", "ali@example.com", True, False)
TypeError: create_user() takes 2 positional arguments but 4 were given
I much prefer a TypeError at the call site over a subtle bug discovered in production weeks later.
Making Every Parameter Keyword-Only
If I want to force all parameters to be passed by keyword, I put the bare * right after self (for methods) or as the very first item in the signature (for plain functions):
def configure(*, host, port, timeout=30):
print(f"host={host}, port={port}, timeout={timeout}")
configure(host="localhost", port=8080)
Any attempt to call configure("localhost", 8080) fails immediately with TypeError: configure() takes 0 positional arguments but 2 were given.
Default Values With Keyword-Only Arguments
Keyword-only parameters can have default values, just like regular ones, and defaults still make them optional:
def resize_image(path, *, width=None, height=None, keep_aspect_ratio=True):
print(f"{path}: {width}x{height}, keep_ratio={keep_aspect_ratio}")
resize_image("photo.jpg", width=800)
Unlike positional-or-keyword parameters, there’s no ordering restriction requiring defaulted keyword-only parameters to come after non-defaulted ones among themselves — you can freely mix defaulted and non-defaulted keyword-only parameters in any order after the *:
def sample(*, a, b=10, c):
print(a, b, c)
sample(a=1, c=3) # b uses its default -- valid even though b (with default) sits between a and c (without defaults)
This is different from the rule for regular positional-or-keyword parameters, where non-default parameters must come before default ones — I found that distinction genuinely useful once I needed several optional, order-independent configuration options.
Combining *args With Keyword-Only Enforcement
If a function already uses *args to collect variable positional arguments, everything defined after *args is automatically keyword-only — no separate bare * needed:
def log_event(event_name, *tags, level="INFO", **metadata):
print(f"[{level}] {event_name} tags={tags} meta={metadata}")
log_event("user_login", "auth", "session", level="DEBUG", user_id=42)
Here, level must always be passed by keyword since it comes after *tags, and the caller can pass zero or more free-form tags positionally before it.
Positional-Only Parameters: The Opposite Enforcement
Python also lets you force the opposite — parameters that must be positional and cannot be passed by keyword — using / (introduced formally in Python 3.8 via PEP 570). I mention this because combining both markers gives you full control over a function’s calling convention:
def divide(numerator, denominator, /, *, round_result=False):
result = numerator / denominator
return round(result) if round_result else result
print(divide(10, 3, round_result=True))
# divide(numerator=10, denominator=3) # TypeError -- these are positional-only
Here, numerator and denominator must be positional (their names are just implementation detail, subject to change), while round_result must be a named keyword (making the call site self-documenting for a boolean flag). I use this combined pattern in library-style functions where I want both readability for flags and freedom to rename simple positional parameters later.
Real Standard Library Examples
Once I started paying attention, I noticed keyword-only enforcement all over the standard library and popular third-party packages:
sorted([3, 1, 2], key=lambda x: -x) # key and reverse are effectively used as keyword-only by convention
print(int("101", base=2)) # base is keyword-friendly
# dataclasses uses keyword-only fields explicitly
from dataclasses import dataclass, field
@dataclass(kw_only=True)
class Config:
host: str
port: int
debug: bool = False
c = Config(host="localhost", port=8080)
The kw_only=True option for dataclass, added in Python 3.10, applies keyword-only enforcement to every generated __init__ parameter automatically — I use it constantly now for configuration objects, since it prevents any ambiguity about field order at construction time.
Internal Working: How Python Tracks This
Function objects store a __kwdefaults__ attribute for keyword-only parameter defaults, separate from __defaults__, which holds only the positional/positional-or-keyword defaults:
def sample(a, b=1, *, c, d=4):
pass
print(sample.__defaults__) # (1,)
print(sample.__kwdefaults__) # {'d': 4}
Using inspect.signature, you can see each parameter’s kind explicitly classified as KEYWORD_ONLY:
import inspect
for name, param in inspect.signature(sample).parameters.items():
print(name, param.kind)
Output:
a POSITIONAL_OR_KEYWORD
b POSITIONAL_OR_KEYWORD
c KEYWORD_ONLY
d KEYWORD_ONLY
Seeing this concretely confirmed that keyword-only enforcement isn’t just a call-site convention — it’s tracked as real metadata on the function object and used directly by the interpreter’s argument-binding logic when a call is made.
Real-World Use Cases
- Boolean flags in any function signature — forcing
keep_aspect_ratio=Trueinstead of a bareTrueat the call site. - Configuration and constructor functions, especially with
dataclass(kw_only=True), to avoid fragile field-order dependencies. - Public library APIs, where forcing keyword arguments for optional parameters lets you add new parameters later without breaking existing positional calls.
- Functions with many optional parameters, where positional calling would require memorizing an arbitrary order.
- Security- or safety-relevant parameters (like
verify_sslordry_run) where an accidental wrong-order positional call could have real consequences.
Best Practices and Common Mistakes
- Force keyword-only status for any parameter whose meaning isn’t obvious from a positional value alone — especially booleans,
None, and numeric flags. - Use
*immediately after the parameters that genuinely make sense positionally (likename,email) and before the ones that need clarity (likeis_admin). - For dataclasses and similar structured objects, prefer
kw_only=Trueover manually documenting “please always use keyword arguments” in a docstring — the language enforces it instead of relying on developer discipline. - Don’t force keyword-only status on every single parameter reflexively — for functions with one or two obviously-ordered arguments (like
add(a, b)), positional calling is perfectly clear and keyword-only enforcement adds unnecessary friction. - Remember that adding a bare
*to an existing function signature is a breaking change for any caller currently using positional arguments for those parameters — coordinate this kind of refactor carefully in shared codebases.
Troubleshooting Tips
If you get TypeError: func() takes 2 positional arguments but 4 were given on a call that used to work, check whether a * was recently added to the function signature, forcing some arguments to be passed by keyword now.
If TypeError: func() missing 1 required keyword-only argument, you called the function without one of the parameters defined after *, and it has no default.
If you’re not sure whether a parameter is keyword-only, inspect it directly with inspect.signature(func).parameters['name'].kind.
FAQs
What Python version introduced the bare * for keyword-only arguments? Keyword-only arguments were introduced in Python 3.0 via PEP 3102; positional-only parameters using / came later, in Python 3.8, via PEP 570.
Can I force keyword-only arguments in a lambda? Yes — lambda x, *, y: x + y is valid syntax, though lambdas with keyword-only parameters are uncommon in practice since lambdas are typically used for very simple, short expressions.
Does forcing keyword arguments affect performance? No measurable difference — keyword-only enforcement is purely a call-site restriction; the underlying argument-binding cost is essentially the same as for regular parameters.
Is kw_only=True in dataclass the same mechanism as a bare * in a regular function? Yes, conceptually — dataclass(kw_only=True) generates an __init__ method whose parameters are all keyword-only, using exactly the same interpreter-level mechanism as a manually written function with a bare *.
Summary
Forcing named parameters with Python’s keyword-only argument syntax — a bare * in the function signature, or automatically after *args — turns ambiguous, error-prone positional calls into clear, self-documenting ones. Combined with positional-only parameters (/) for the opposite effect, and dataclass(kw_only=True) for structured configuration objects, this gives me precise control over how a function can be called. After being burned by an unreadable positional call site early in my career, I now treat keyword-only enforcement as a default tool for any function with more than one boolean or same-type parameter — it costs nothing and prevents an entire category of silent bugs.