069. '*Args (Variable Positional Arguments)'
Accept arbitrary numbers of positional arguments
069. ‘*Args (Variable Positional Arguments)’
Aryan’s monitor has a helper that averages memory readings. Sometimes there are 5 readings in the window, sometimes 60 — the count varies. With fixed parameters he’d need a different function for each count. With *args he writes it once:
def average_pct(*readings):
"""Average any number of memory percentage readings."""
if not readings:
return 0.0
return sum(readings) / len(readings)
# Works for any call count
avg5 = average_pct(72.1, 73.4, 71.8, 74.0, 72.9)
avg1 = average_pct(85.0)
avg0 = average_pct() # returns 0.0
# Pass a pre-existing list with unpacking
window = [72.1, 73.4, 71.8]
avg = average_pct(*window) # unpack list → positional argsHe also uses *args for a flexible alert function that can flag multiple processes at once:
def flag_processes(*names):
return [name for name in names if name in WATCHED_SET]
flag_processes("Chrome", "Slack", "malware.exe")Inside the function, *args (or *readings, *names) is always a tuple — you can iterate it, call len(), sum(), min(), max().
💡 Fun fact: Variadic functions (accepting a variable number of arguments) exist in almost every language. In C, printf(format, ...) is variadic — but C has no way to know how many arguments were passed, so it relies on the format string to count them. Python’s *args is far safer: len(args) always gives the exact count.
⚠️ Watch out: Inside the function, *args is a tuple, not a list — you cannot append to it or modify it. Beginners often try args.append(x) and get an AttributeError. If you need to modify the collected arguments, convert first: items = list(args).
🤔 Think about it: add_all(*window) unpacks a list into positional arguments, and def add_all(*numbers) repacks them into a tuple. Why does Python do this round-trip instead of just passing the list directly? What does this design decision enable?
Learning objectives
- Use *args to accept variable numbers of positional arguments
- Access *args as a tuple inside the function
- Call functions that use *args with any number of arguments
Key concepts
- *args
- variadic functions
- tuple
Try it
Concept detail
*args in a function definition collects extra positional arguments into a tuple.
def f(*args): — args is always a tuple (possibly empty) def f(a, b, *rest): — a and b are required; rest gets any extras as a tuple
Inside the function: len(args) — how many were passed sum(args) — works because args is a tuple of numbers for x in args: — iterate normally
Calling with a pre-built list (unpacking): items = [1, 2, 3] f(*items) — same as f(1, 2, 3)
Combining with regular parameters: def log(level, *messages): for msg in messages: print(f“[{level}] {msg}“) log(“INFO”, “Starting”, “Ready”, “Running”)
*args vs passing a list: add_all(1, 2, 3) — *args style: clean at call site add_all([1, 2, 3]) — list style: requires the caller to build a list first
Use *args when the function naturally handles “zero or more” of the same thing.
Solution
def add_all(*numbers):
return sum(numbers)
def multiply_all(*numbers):
result = 1
for n in numbers:
result *= n
return result
def stats(*numbers):
return {
"min": min(numbers),
"max": max(numbers),
"mean": sum(numbers) / len(numbers),
"count": len(numbers),
}Tests
def test_add_two():
assert add_all(1, 2) == 3
def test_add_many():
assert add_all(1, 2, 3, 4, 5) == 15
def test_add_one():
assert add_all(42) == 42
def test_multiply_two():
assert multiply_all(6, 7) == 42
def test_multiply_all():
assert multiply_all(2, 3, 4) == 24
def test_multiply_one():
assert multiply_all(5) == 5
def test_stats():
result = stats(10, 20, 30, 40)
assert result["min"] == 10
assert result["max"] == 40
assert result["mean"] == 25.0
assert result["count"] == 4
def test_stats_two():
result = stats(3, 7)
assert result["min"] == 3
assert result["max"] == 7
assert result["count"] == 2