Forcing the Use of Named Parameters in Python: Complete Keyword-Only Arguments Implementation Guide

Forcing the use of named parameters in python

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=True instead of a bare True at 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_ssl or dry_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 (like name, email) and before the ones that need clarity (like is_admin).
  • For dataclasses and similar structured objects, prefer kw_only=True over 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.

References

Total
0
Shares

Leave a Reply

Previous Post
Returning values from functions in python

Returning Values from Functions in Python: Complete Return Statement and Multiple Return Values Guide

Next Post
Recursive Lambda using assigned variable in python

Recursive Lambda Using Assigned Variable in Python: Complete Anonymous Recursive Function Implementation Guide

Related Posts