Skip to main content

Python Argument Kinds

Satellite · Functions

Argument Kinds

Design APIs that communicate intent—control how callers pass arguments using positional-only markers, keyword-only sections, and variadic parameters.

The five kinds, in order

A Python signature can declare five kinds of parameters, and they must appear in this order:

def func(pos_only, /, pos_or_kw, *args, kw_only, **kwargs):
...
  • Everything before / is positional-only (Python 3.8+).
  • Between / and * (or *args) parameters are positional-or-keyword — the default kind.
  • *args collects extra positional arguments into a tuple.
  • After * (or *args), parameters are keyword-only.
  • **kwargs collects extra keyword arguments into a dict.

Positional-or-keyword (the default)

With no markers, callers choose either style:

def greet(name, greeting='Hello'):
return f'{greeting}, {name}!'

print(greet('Ada')) # -> Hello, Ada!
print(greet(name='Ada', greeting='Hi')) # -> Hi, Ada!

Flexible — but it also means the parameter names become part of your public API: renaming name to username later breaks every caller that used greet(name=...).

Positional-only: /

Parameters before / can only be passed by position. Use this when the names are implementation details you want the freedom to change:

def ratio(numerator, denominator, /):
return numerator / denominator

print(ratio(10, 4)) # -> 2.5

ratio(numerator=10, denominator=4)
# TypeError: ratio() got some positional-only arguments passed as
# keyword arguments: 'numerator, denominator'

The standard library uses this heavily. sorted declares its first parameter positional-only, which is why this fails:

sorted(iterable=[3, 1, 2])
# TypeError: sorted expected 1 argument, got 0

Variadic positional: *args

A parameter prefixed with * collects any surplus positional arguments into a tuple:

def total(*numbers):
return sum(numbers)

print(total(1, 2, 3)) # -> 6
print(total()) # -> 0

Inside the function, numbers is a plain tuple: (1, 2, 3) for the first call, () for the second.

Keyword-only: after *

A bare * in the signature means everything after it must be passed by keyword. Use it for options whose meaning would be unclear as bare values at the call site:

def connect(host, *, timeout=5, retries=3):
return f'{host} timeout={timeout} retries={retries}'

print(connect('db.local', timeout=10)) # -> db.local timeout=10 retries=3

connect('db.local', 10)
# TypeError: connect() takes 1 positional argument but 2 were given

That error is the feature working: connect('db.local', 10) gives the reader no clue whether 10 is a timeout, a retry count, or a port. Forcing timeout=10 makes every call self-documenting.

Variadic keyword: **kwargs

A parameter prefixed with ** collects surplus keyword arguments into a dict, preserving the order they were passed:

def tag(name, **attrs):
parts = ' '.join(f'{k}="{v}"' for k, v in attrs.items())
return f'<{name} {parts}>'

print(tag('img', src='logo.png', alt='Logo'))
# -> <img src="logo.png" alt="Logo">

A realistic combined signature

help(sorted) shows a signature mixing the markers:

sorted(iterable, /, *, key=None, reverse=False)

The data argument is positional-only; the options are keyword-only. Here is the same design in a custom function that uses all five kinds:

def query(table, /, limit=10, *columns, order_by=None, **filters):
print('table: ', table)
print('limit: ', limit)
print('columns: ', columns)
print('order_by:', order_by)
print('filters: ', filters)

query('users', 5, 'name', 'email', order_by='name', active=True)
# table: users
# limit: 5
# columns: ('name', 'email')
# order_by: name
# filters: {'active': True}

Reading the call: 'users' must be positional (table is before /), 5 fills limit, the remaining positionals land in columns, order_by must be a keyword (it follows *columns), and the unknown keyword active falls through to filters.

Frequently Asked Questions

What does the / in help(sorted) mean?

Everything before the / is positional-only: you cannot pass it by name. That is why sorted(iterable=[3, 1, 2]) raises TypeError: sorted expected 1 argument, got 0 — the keyword does not match any parameter sorted will accept by name.

sorted([3, 1, 2])            # -> [1, 2, 3]
sorted(iterable=[3, 1, 2])
# TypeError: sorted expected 1 argument, got 0

Do *args and **kwargs have to use those names?

No — only the * and ** prefixes matter; args and kwargs are just the conventional names. def f(*values, **options) works identically. Stick with the convention unless a more specific name genuinely improves readability.

Can I put a parameter after *args?

Yes, and it automatically becomes keyword-only, since every positional argument beyond the named ones is swallowed by *args. This is a common way to add required options to a variadic function.

def join_paths(*parts, sep='/'):
return sep.join(parts)

print(join_paths('usr', 'local', 'bin')) # -> usr/local/bin
print(join_paths('a', 'b', sep='.')) # -> a.b

Next up in your learning path