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