Skip to content
  • Argument Kinds: *args, **kwargs, / and * · Python

Argument Kinds: *args, **kwargs, / and *

LearnViaNotes @learnvianotes· 5 min read

A Python signature can say exactly how each parameter may be passed — by position, by name, or either — and can soak up any number of extras with *args and **kwargs.

The full signature, in order

def f(pos_only, /, pos_or_kw, *args, kw_only, **kwargs)

PartSyntaxHow callers pass it
Positional-onlybefore /Only by position: f(1) — f(a=1) is an error
Positional-or-keywordbetween / and *Either: f(1) or f(b=1) — the default kind
Variadic positional*argsExtra positional values, collected into a tuple
Keyword-onlyafter * or *argsOnly by name: f(key=1)
Variadic keyword**kwargsExtra named values, collected into a dict (must be last)
Python
def f(a, b=1, /, c=2, *args, d, e=5, **kwargs):
    return f"a={a} b={b} c={c} args={args} d={d} e={e} kwargs={kwargs}"

print(f(10, d=4))
# a=10 b=1 c=2 args=() d=4 e=5 kwargs={}
print(f(10, 20, 30, 40, 50, d=4, z=9))
# a=10 b=20 c=30 args=(40, 50) d=4 e=5 kwargs={'z': 9}
Output
a=10 b=1 c=2 args=() d=4 e=5 kwargs={}
a=10 b=20 c=30 args=(40, 50) d=4 e=5 kwargs={'z': 9}

Keyword-only parameters (*)

A bare * ends the positional parameters. Everything after it must be named at the call site — ideal for flags and options that would be unreadable as bare True/False.

Python
def connect(host, port, *, timeout=10, retries=3):
    return f"{host}:{port} t={timeout} r={retries}"

print(connect("db", 5432, timeout=2))     # db:5432 t=2 r=3
try:
    connect("db", 5432, 2)                # which option is 2? Python refuses to guess
except TypeError as e:
    print(e)   # connect() takes 2 positional arguments but 3 were given

Positional-only parameters (/, 3.8+)

Parameters before / can't be passed by name. Many built-ins work this way (len(obj=…) is an error). It lets a library rename parameters later without breaking callers, and frees the name for **kwargs.

Python
def distance(x, y, /):
    return (x * x + y * y) ** 0.5

print(distance(3, 4))                  # 5.0
try:
    distance(x=3, y=4)
except TypeError as e:
    print(type(e).__name__)            # TypeError

def format_row(name, /, **fields):     # 'name' can ALSO appear as a field
    return f"{name}: {fields}"

print(format_row("user", name="asha"))  # user: {'name': 'asha'}

*args and **kwargs

Python
def total(*nums):
    return sum(nums)

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

print(total(), total(1, 2, 3))                 # 0 6
print(tag("a", href="/home", id="nav"))        # <a href="/home" id="nav">

Unpacking at the call site

Python
def point(x, y, z=0):
    return (x, y, z)

coords = [1, 2]
opts = {"z": 9}
print(point(*coords))                  # (1, 2, 0)
print(point(*coords, **opts))          # (1, 2, 9)
print(point(**{"x": 5, "y": 6}))       # (5, 6, 0)

defaults = {"z": 1}
overrides = {"z": 3}
print(point(0, 0, **{**defaults, **overrides}))   # (0, 0, 3)   ← merge first, later wins
print(max(*[4, 8, 2]), *"abc")         # 8 a b c
Python
def point(x, y, z=0):
    return (x, y, z)
try:
    point(1, 2, z=0, **{"z": 9})
except TypeError as e:
    print(type(e).__name__, "multiple values" in str(e))   # TypeError True
try:
    point(1, x=1)
except TypeError as e:
    print(e)   # point() got multiple values for argument 'x'

Forwarding arguments

*args, **kwargs in a wrapper accepts anything and passes it on unchanged — the core trick behind decorators, proxies and subclass constructors.

Python
import functools

def logged(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        shown = [repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]
        print(f"{func.__name__}({', '.join(shown)})")
        return func(*args, **kwargs)
    return wrapper

@logged
def power(base, exp=2):
    return base ** exp

print(power(3, exp=3))
Output
power(3, exp=3)
27

Inspecting a signature

Python
import inspect

def f(a, /, b, *args, c, d=1, **kw):
    pass

sig = inspect.signature(f)
print(str(sig))                  # (a, /, b, *args, c, d=1, **kw)
print([p.kind.name for p in sig.parameters.values()])
# ['POSITIONAL_ONLY', 'POSITIONAL_OR_KEYWORD', 'VAR_POSITIONAL', 'KEYWORD_ONLY', 'KEYWORD_ONLY', 'VAR_KEYWORD']
bound = sig.bind(1, 2, 3, c=4)
print(bound.arguments)           # {'a': 1, 'b': 2, 'args': (3,), 'c': 4}
Output
(a, /, b, *args, c, d=1, **kw)
['POSITIONAL_ONLY', 'POSITIONAL_OR_KEYWORD', 'VAR_POSITIONAL', 'KEYWORD_ONLY', 'KEYWORD_ONLY', 'VAR_KEYWORD']
{'a': 1, 'b': 2, 'args': (3,), 'c': 4}

Defaults are evaluated once

Python
import time

def stamp(when=time.time()):      # evaluated ONCE, at def time
    return when

print(stamp() == stamp())         # True   ← same frozen value every call

def stamp_ok(when=None):
    return time.time() if when is None else when

What are *args and **kwargs?

What do / and * mean in a parameter list?

Why make parameters keyword-only?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report