Skip to main content Brad's PyNotes

PEP 570 Positional Only

TL;DR

PEP 570 introduced the / separator to mark positional-only parameters, giving API designers control over function call semantics and preventing keyword argument usage.

Interesting!

Many built-in Python functions like len(), abs(), and pow() have always been positional-only - PEP 570 brought this capability to user-defined functions!

Basic Syntax

python code snippet start

def function(pos_only, /, pos_or_kwd, *, kwd_only):
    pass

# pos_only:   must be positional
# pos_or_kwd: can be positional or keyword  
# kwd_only:   must be keyword-only

# Valid calls:
function(1, 2, kwd_only=3)
function(1, pos_or_kwd=2, kwd_only=3)

# Invalid calls:
# function(pos_only=1, pos_or_kwd=2, kwd_only=3)  # Error!

python code snippet end

Preventing Keyword Arguments

python code snippet start

def greet(name, /):
    """Name must be passed positionally."""
    return f"Hello, {name}!"

# Works
print(greet("Alice"))  # Hello, Alice!

# Fails - TypeError
try:
    print(greet(name="Bob"))
except TypeError as e:
    print(f"Error: {e}")  # greet() got some positional-only arguments passed as keyword

python code snippet end

API Design Flexibility

python code snippet start

# Library author can change parameter names without breaking compatibility
def calculate_distance(x1, y1, x2, y2, /):
    """Calculate distance between two points."""
    return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5

# Later, author can rename parameters internally
def calculate_distance(start_x, start_y, end_x, end_y, /):
    """Calculate distance between two points."""
    return ((end_x - start_x) ** 2 + (end_y - start_y) ** 2) ** 0.5

# Users' code still works - they can only call positionally:
distance = calculate_distance(0, 0, 3, 4)  # Always works

python code snippet end

Mixed Parameter Types

python code snippet start

def complex_function(pos_only, /, normal, *, kwd_only):
    """Demonstrate all parameter types."""
    return f"pos_only={pos_only}, normal={normal}, kwd_only={kwd_only}"

# Valid calls:
print(complex_function(1, 2, kwd_only=3))
print(complex_function(1, normal=2, kwd_only=3))

# Invalid calls would raise TypeError:
# complex_function(pos_only=1, normal=2, kwd_only=3)  # pos_only as keyword
# complex_function(1, 2, 3)  # kwd_only not provided as keyword

python code snippet end

Preventing Name Conflicts

python code snippet start

def process_data(data, /, **kwargs):
    """Process data with optional keyword arguments."""
    print(f"Processing: {data}")
    
    # Safe to use 'data' in kwargs without conflict
    if 'data_format' in kwargs:
        print(f"Format: {kwargs['data_format']}")

# Usage
process_data([1, 2, 3], data_format="json", data_source="api")
# The 'data' parameter won't conflict with 'data_format' or 'data_source'

python code snippet end

Real-World Examples

python code snippet start

def pow(base, exp, mod=None, /):
    """Replicate built-in pow() signature."""
    if mod is None:
        return base ** exp
    return (base ** exp) % mod

def divmod(a, b, /):
    """Replicate built-in divmod() signature."""
    return (a // b, a % b)

def range(start, stop=None, step=1, /):
    """Replicate built-in range() signature."""
    if stop is None:
        stop = start
        start = 0
    
    result = []
    current = start
    while current < stop:
        result.append(current)
        current += step
    return result

# All called positionally like built-ins:
print(pow(2, 3))      # 8
print(divmod(10, 3))  # (3, 1)
print(range(5))       # [0, 1, 2, 3, 4]

python code snippet end

Function Registration Pattern

python code snippet start

class EventHandler:
    def __init__(self):
        self.handlers = {}
    
    def register(self, event_type, /, handler=None):
        """Register event handler."""
        def decorator(func):
            self.handlers[event_type] = func
            return func
        
        if handler is None:
            return decorator
        else:
            return decorator(handler)

# Usage patterns:
event_system = EventHandler()

# As decorator
@event_system.register("click")
def handle_click():
    print("Click handled!")

# Direct registration  
def handle_hover():
    print("Hover handled!")

event_system.register("hover", handle_hover)

# event_type is positional-only, so no confusion with handler parameter

python code snippet end

Performance Benefits

python code snippet start

def fast_math_operation(x, y, z, /):
    """Positional-only parameters are slightly faster to process."""
    return x * y + z

# Python can optimize argument parsing since it knows
# these will never be passed as keywords
result = fast_math_operation(2, 3, 4)  # result = 10

python code snippet end

Backwards Compatibility

python code snippet start

# Library evolution example
class DatabaseConnection:
    # Version 1.0
    def query(self, sql, /):
        pass
    
    # Version 2.0 - can add parameters without breaking existing code
    def query(self, sql, /, timeout=30, retries=3):
        pass
    
    # Users' code still works:
    # conn.query("SELECT * FROM users")
    
    # But they can't accidentally pass:
    # conn.query(sql="SELECT * FROM users")  # This would error

python code snippet end

When to Use Positional-Only

python code snippet start

# Good candidates for positional-only:
def min_max(a, b, /):
    """Parameter names don't add meaning."""
    return (min(a, b), max(a, b))

def rgb_to_hex(red, green, blue, /):
    """Natural order, names are obvious from context."""
    return f"#{red:02x}{green:02x}{blue:02x}"

def apply_operation(func, value, /):
    """Function clearly operates on value."""
    return func(value)

# When NOT to use:
def create_user(name, email, age):  # Keep as keyword-capable
    """These benefit from keyword clarity."""
    pass

# create_user(name="Alice", email="alice@example.com", age=25)  # Clear intent

python code snippet end

Positional-only parameters give library authors precise control over their APIs while maintaining backwards compatibility and preventing common parameter-related bugs!

Reference: PEP 570 – Python Positional-Only Parameters