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
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), }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 testsImplement 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)
Sign up to check the hidden tests and save your progress. Sign up