Blog

args and kwargs in Python

A star in front of a parameter collects the leftover positional arguments into a tuple. Two stars collect the leftover keyword arguments into a dict.

The names args and kwargs are only convention. The stars do the work.

One star collects positional arguments

Inside the function, args is a plain tuple. It is empty when nothing extra was passed.

# *args collects positional arguments
def total(*args):
    return sum(args), type(args).__name__

print(total(1, 2, 3))
print(total())

It prints:

(6, 'tuple')
(0, 'tuple')

Two stars collect keyword arguments

kwargs is a plain dict, in the order the arguments were given.

# **kwargs collects keyword arguments
def describe(**kwargs):
    return kwargs, type(kwargs).__name__

print(describe(name='ada', age=36))

It prints:

({'name': 'ada', 'age': 36}, 'dict')

The order is fixed

Normal parameters, then *args, then keyword-only parameters, then **kwargs. Python enforces this.

# the order is fixed
def mixed(first, *args, key=None, **kwargs):
    return first, args, key, kwargs

print(mixed(1, 2, 3, key='k', extra=True))

It prints:

(1, (2, 3), 'k', {'extra': True})

Anything after *args can only be passed by name.

A bare star forces keyword-only arguments

If you do not want the extra positional arguments but do want callers to name things, use a bare star.

# a bare star forces keyword-only arguments
def connect(host, *, port=5432, timeout=30):
    return host, port, timeout

print(connect('localhost', port=5433))
try:
    connect('localhost', 5433)
except TypeError as err:
    print(type(err).__name__ + ':', err)

It prints:

('localhost', 5433, 30)
TypeError: connect() takes 1 positional argument but 2 were given

This is worth doing for any function with several options, because connect(host, 5433, 60) tells the reader nothing.

The same stars unpack at the call site

In a call, the stars mean the opposite. They spread a sequence or a dict into arguments.

# unpacking at the call site
def point(x, y, z):
    return x + y + z

coords = [1, 2, 3]
named = {'x': 1, 'y': 2, 'z': 3}
print(point(*coords), point(**named))

It prints:

6 6

Passing everything through

This is the pattern behind every wrapper and decorator. Accept anything, forward it unchanged.

# forwarding everything to another function
def logged(fn, *args, **kwargs):
    print('calling', fn.__name__, 'with', args, kwargs)
    return fn(*args, **kwargs)

print(logged(point, 1, 2, z=3))

It prints:

calling point with (1, 2) {'z': 3}
6

The wrapper does not need to know the signature of the function it wraps.

What to remember

  • *args is a tuple of extra positional arguments, **kwargs is a dict of extra keyword arguments.
  • The order is: normal, *args, keyword-only, **kwargs.
  • A bare * makes the parameters after it keyword-only.
  • At a call site the stars unpack instead of collect.

If you find yourself writing *args, **kwargs on a function that is not a wrapper, it is usually worth naming the parameters instead. The signature is documentation.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

Leave a Reply