Python

Functions, arguments, and *args / **kwargs

Positional against keyword arguments, star-unpacking, and what a decorator actually is.

After this lesson you can

  • Tell positional-only, keyword-only, and either-way parameters apart
  • Use *args and **kwargs to accept an arbitrary number of arguments
  • Explain what a decorator does to the function underneath it

A parameter can usually be filled by position or by name.

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

greet("Nino")                    # "Hello, Nino"
greet("Nino", "Hi")               # "Hi, Nino"
greet(name="Nino", greeting="Hi") # "Hi, Nino" — same call, by keyword

def greet(name, /, greeting) forces name to be positional-only — the caller cannot write greet(name="Nino", ...). def greet(*, greeting) forces greeting to be keyword-only. Both exist to make an API's intent explicit rather than leaving it to convention.

*args and **kwargs

*args collects any number of extra positional arguments into a tuple. **kwargs collects any number of extra keyword arguments into a dict.

def total(*args, **kwargs):
    return sum(args) + sum(kwargs.values())

total(1, 2, 3, extra=10)   # 16

The names args and kwargs are convention, not syntax — the * and ** are what matter. The same stars unpack in the other direction, at a call site:

nums = [1, 2, 3]
print(*nums)          # print(1, 2, 3) — three separate arguments

opts = {"greeting": "Hi"}
greet("Nino", **opts)  # greet(name="Nino", greeting="Hi")

Try it

Any number of numbers, plus named bonusespython-3.12
def total(*args, **kwargs):    return sum(args) + sum(kwargs.values())  def run():    return {        "just_args": total(1, 2, 3),        "with_bonus": total(1, 2, bonus=10, extra=5),    }
What to look for

Closures over loop-bound state

A function defined inside another function keeps access to the enclosing function's variables even after the outer one has returned — the same idea JavaScript's closures cover, and it has the identical trap in a loop:

def make_multipliers():
    return [lambda x: x * i for i in range(3)]

fns = make_multipliers()
[f(10) for f in fns]   # [20, 20, 20] — every lambda shares the same i

Python's for variable is not scoped per iteration the way a fresh binding would be; all three lambdas close over the same i, and by the time any of them run the loop has finished with i at 2. The fix is to capture the value as a default argument, evaluated once per lambda at definition time:

[lambda x, i=i: x * i for i in range(3)]   # [0, 10, 20]

Decorators wrap a function, they do not change its source

def log_calls(fn):
    def wrapper(*args, **kwargs):
        print(f"calling {fn.__name__}")
        return fn(*args, **kwargs)
    return wrapper

@log_calls
def add(a, b):
    return a + b

@log_calls above add is exactly add = log_calls(add). wrapper is what add actually refers to afterwards; it accepts anything via *args, **kwargs and forwards it, which is why a decorator can wrap a function it knows nothing about the signature of.

Try it yourself

2 visible tests · 2 hidden tests

Implement entryPoint(*args). args may hold any number of numbers, including none. Return a dict with "count" (how many arguments came in) and "total" (their sum, 0 for none).

  • entryPoint(1, 2, 3)
  • entryPoint(42)
Loading editor…

Sign up to check the hidden tests and save your progress. Sign up