Keyword-only parameters are a powerful feature introduced in Python 3.0 that enforces clarity and intentionality in function calls. They are defined syntactically by placing them after a single bare * in the function’s parameter list. This * acts as a syntactic separator, indicating that all subsequent parameters must be passed using their keyword. This design prevents them from being passed as positional arguments, which is a common source of errors in APIs where certain parameters are optional but carry significant meaning.

The primary motivation for keyword-only arguments is to create more robust and self-documenting interfaces. When a function has many optional parameters, especially ones with default values, calling it with a long list of positional arguments becomes error-prone. It’s easy to forget the order of the optional flags. By forcing the user to specify the parameter name, the code becomes instantly more readable and less susceptible to subtle bugs caused by misplaced arguments. Furthermore, it allows library authors to add new optional parameters to existing functions without breaking backward compatibility for existing code that uses positional arguments for the required parameters.

Defining Keyword-Only Parameters

The syntax for defining keyword-only parameters is straightforward. The bare * “consumes” any remaining positional arguments (if combined with *args) or simply acts as a marker if no other positional parameters are present.

def create_user(name, *, admin=False, active=True, user_id=None):
    """Create a user. The privileges and status must be explicitly named."""
    print(f"Creating User: {name} (ID: {user_id})")
    print(f" - Admin: {admin}")
    print(f" - Active: {active}")
    return {'name': name, 'admin': admin, 'active': active, 'id': user_id}

# Valid calls: Keywords are required for admin, active, and user_id.
user1 = create_user("Alice")
user2 = create_user("Bob", admin=True)
user3 = create_user("Charlie", user_id=1001, active=False)

# Invalid call: Trying to pass a keyword-only argument by position.
# This will raise a TypeError.
try:
    create_user("Dave", True)
except TypeError as e:
    print(f"Error: {e}") # Error: create_user() takes 1 positional argument but 2 were given

Combining with *args and **kwargs

Keyword-only parameters are most powerful when used in conjunction with *args and **kwargs. This pattern is ubiquitous in advanced libraries and frameworks. The *args collects any number of positional arguments, the * marks the end of the positional argument list, and the keyword-only parameters follow. **kwargs can then be used to collect any additional keyword arguments that weren’t explicitly defined.

def sophisticated_logger(*messages, level="INFO", timestamp=True, **metadata):
    """Logs messages with configurable options and arbitrary metadata."""
    prefix = f"[{level}]" + (" [2023-10-27 10:00:00]" if timestamp else "")
    print(prefix, *messages)
    if metadata:
        print(f"Additional metadata: {metadata}")

# The first arguments are captured by *messages.
# 'level' and 'timestamp' are explicitly defined keyword-only args.
# 'user' and 'app_version' are captured by **kwargs.
sophisticated_logger("System", "started", successfully=True, level="DEBUG", user="admin")
sophisticated_logger("A critical error occurred!", level="ERROR", timestamp=False, app_version="1.5.2")

Common Pitfalls and Best Practices

A common pitfall is misunderstanding the placement of the *. It must come after all intended positional parameters (including those with defaults) and before the keyword-only parameters. Another subtle error occurs when you forget the * entirely, which would cause the subsequent parameters to be treated as standard optional parameters that can still be passed by position.

Best Practice 1: Use keyword-only arguments for all optional parameters that are not mere simple flags but carry significant meaning. For example, a threshold value or a mode selector (mode='read' vs mode='write') are excellent candidates, as their purpose is ambiguous when passed positionally.

Best Practice 2: When designing a function that accepts *args, always consider using keyword-only arguments for any subsequent options. This prevents the options from being accidentally consumed into the *args tuple. This is a classic and hard-to-debug issue that keyword-only arguments elegantly solve.

# Problematic design without keyword-only args
def process_items(*items, reverse=False):
    # If someone mistakenly calls process_items(1, 2, True),
    # 'True' becomes part of the 'items' tuple, and reverse remains False.
    if reverse:
        items = items[::-1]
    print(items)

process_items(1, 2, True)  # Output: (1, 2, True) - Not what was intended!

# Robust design with keyword-only args
def process_items_fixed(*items, *, reverse=False):
    # Now, 'reverse' CANNOT be passed by position.
    if reverse:
        items = items[::-1]
    print(items)

process_items_fixed(1, 2, reverse=True)  # Output: (2, 1) - Correct!
# process_items_fixed(1, 2, True) # This would now raise a TypeError.

In summary, keyword-only parameters are an essential tool for writing clear, explicit, and future-proof Python code. They move Python away from the pitfalls of positional argument overload and towards a more keyword-centric, self-documenting API style that is easier to use correctly and harder to use incorrectly.