Positional-only parameters, introduced in Python 3.8 via the / character in a function signature, represent a significant and deliberate shift in how a function’s interface can be designed. They enforce a specific calling convention, requiring that certain arguments be passed by position and never by keyword. This feature is not about enabling new functionality but about providing API designers with a tool for clarity, robustness, and backward compatibility.

The primary motivation for positional-only parameters is to give library and framework authors greater control over their public APIs. Before Python 3.8, a parameter’s name was part of its public contract. If a user started passing an argument by keyword, the author could not later change that parameter’s name without breaking the user’s code. By marking parameters as positional-only, the name is effectively made private and can be changed in future versions without affecting any existing calls that use the positional convention. This is why you see heavy use of / in built-in functions like len(), print(), and range(); their authors retain the freedom to refactor.

Syntax and Basic Usage

The / is placed in the parameter list to indicate that all parameters to its left are strictly positional-only. Any attempt to pass these arguments using a keyword will result in a TypeError.

def power_calculator(voltage, current, /, power_factor=1.0):
    """Calculate power. voltage and current are positional-only."""
    return voltage * current * power_factor

# Valid calls: Positional arguments
result1 = power_calculator(120, 2)          # 240.0
result2 = power_calculator(120, 2, 0.95)    # 228.0

# Invalid calls: Keyword arguments for positional-only parameters
try:
    power_calculator(voltage=120, current=2)  # TypeError!
except TypeError as e:
    print(e)  # power_calculator() got some positional-only arguments passed as keyword arguments: 'voltage, current'

# The parameter to the right of / can be used as a keyword
valid_call = power_calculator(240, 5, power_factor=0.9)  # 1080.0

In this example, voltage and current are locked to positional passing. The power_factor parameter, appearing after the /, can be passed either by position or by keyword.

Interaction with Other Parameter Types

The / marker works in concert with * and *args to create a complete function signature specification. The order of parameters in a sophisticated function definition follows a specific sequence: positional-only, standard (positional-or-keyword), *args, keyword-only, and finally **kwargs.

def comprehensive_example(a, b, /, pos_or_kw, *, kw_only, **kwargs):
    """
    a, b: Positional-only
    pos_or_kw: Positional-or-keyword
    kw_only: Keyword-only
    kwargs: Arbitrary keyword arguments
    """
    print(f"a={a}, b={b}, pos_or_kw={pos_or_kw}, kw_only={kw_only}, kwargs={kwargs}")

# Valid call
comprehensive_example(1, 2, 3, kw_only=4, extra=5)  # a=1, b=2, pos_or_kw=3, kw_only=4, kwargs={'extra': 5}
comprehensive_example(1, 2, pos_or_kw=3, kw_only=4) # Also valid

# Invalid calls
comprehensive_example(a=1, b=2, pos_or_kw=3, kw_only=4)  # TypeError: a and b are positional-only
comprehensive_example(1, 2, 3, 4)  # TypeError: missing required keyword-only argument 'kw_only'

This structure provides maximum flexibility and control, allowing an author to dictate precisely how each part of their function must be called.

Common Pitfalls and Best Practices

A common pitfall is forgetting that the parameter names before / are essentially invisible to the caller in a keyword context. This makes function calls less self-documenting. Therefore, it’s a best practice to use positional-only parameters for arguments that have a natural and obvious order (e.g., start, stop, step in a range-like function) or for very common operations where the order is well-established.

Another crucial best practice is thorough documentation. Since users cannot use keyword syntax for these parameters, the docstring must be exceptionally clear about the expected order and purpose of each positional-only argument.

def create_vector(x, y, z, /, *, normalize=False):
    """
    Create a 3D vector.

    Parameters
    ----------
    x : float
        X-coordinate (positional-only).
    y : float
        Y-coordinate (positional-only).
    z : float
        Z-coordinate (positional-only).
    normalize : bool, optional, keyword-only
        If True, normalize the vector to unit length. Default is False.

    Returns
    -------
    list[float]
        The vector [x, y, z] (or normalized version).
    """
    vector = [x, y, z]
    if normalize:
        magnitude = (x**2 + y**2 + z**2) ** 0.5
        vector = [comp / magnitude for comp in vector]
    return vector

# The call is concise and the order is clear from context.
my_vector = create_vector(1.0, 2.0, 3.0, normalize=True)

The decision to use positional-only parameters is a deliberate design choice. They are ideal for creating cleaner, more robust APIs, especially when the parameter names are likely to be refactored or when mimicking the calling convention of existing C-based built-in functions. For most user-defined functions where clarity via keyword arguments is a benefit, the standard positional-or-keyword parameters remain the preferred and most intuitive option.