Skip to content
elephantoo

*args, **kwargs & lambdas

Lesson 13 of 38 13 min read

Flexible parameters, argument unpacking, forwarding calls, lambdas, key functions, map and filter.


You already know how to define functions with fixed parameters. But what about print(), which accepts any number of values? Or a wrapper that forwards whatever arguments it receives to another function? This lesson covers Python's flexible argument syntax — *args and **kwargs — plus lambdas, tiny anonymous functions that shine when passing behaviour into other functions.

*args: any number of positional arguments#

A parameter prefixed with * collects all extra positional arguments into a tuple:

Python
def total(*numbers):
    print(type(numbers), numbers)
    return sum(numbers)


print(total(1, 2, 3))
print(total(10))
print(total())
Output
<class 'tuple'> (1, 2, 3)
6
<class 'tuple'> (10,)
10
<class 'tuple'> ()
0

The name args is only a convention — *numbers is clearer here. You can combine regular parameters with *args; the regular ones are filled first:

Python
def log(level, *messages):
    print(f"[{level.upper()}]", *messages)


log("info", "server started", "on port", 8000)
Output
[INFO] server started on port 8000

**kwargs: any number of keyword arguments#

Two stars collect extra keyword arguments into a dict:

Python
def build_profile(name, **details):
    profile = {"name": name}
    profile.update(details)
    return profile


print(build_profile("Ada", city="London", born=1815, field="maths"))
Output
{'name': 'Ada', 'city': 'London', 'born': 1815, 'field': 'maths'}

The full parameter order#

When you mix everything, the order is fixed:

Python
def f(pos_only, /, normal, *args, kw_only, **kwargs):
    print(pos_only, normal, args, kw_only, kwargs)


f(1, 2, 3, 4, kw_only=5, extra=6)
Output
1 2 (3, 4) 5 {'extra': 6}
  1. positional-only parameters (before /)
  2. normal parameters
  3. *args
  4. keyword-only parameters (anything after *args or a bare *)
  5. **kwargs

In practice you rarely need all five at once — but reading library signatures becomes much easier once you know them.

Unpacking at the call site#

The same symbols work in reverse when calling a function: * spreads an iterable into positional arguments, and ** spreads a dict into keyword arguments.

Python
def connect(host, port, timeout=10):
    return f"connecting to {host}:{port} (timeout {timeout}s)"


address = ("db.example.com", 5432)
options = {"timeout": 3}

print(connect(*address))
print(connect(*address, **options))

config = {"host": "localhost", "port": 3306}
print(connect(**config))
Output
connecting to db.example.com:5432 (timeout 10s)
connecting to db.example.com:5432 (timeout 3s)
connecting to localhost:3306 (timeout 10s)

Unpacking is also handy for merging data: [*list_a, *list_b] and {**defaults, **overrides}.

Forwarding arguments#

The most important real-world use of *args, **kwargs is writing a function that wraps another and passes everything through unchanged. You'll use exactly this pattern when writing decorators:

Python
import time


def timed_call(func, *args, **kwargs):
    start = time.perf_counter()
    result = func(*args, **kwargs)          # forward everything
    elapsed = time.perf_counter() - start
    print(f"{func.__name__} took {elapsed:.4f}s")
    return result


def slow_add(a, b, delay=0.1):
    time.sleep(delay)
    return a + b


print(timed_call(slow_add, 2, 3, delay=0.05))
Output
slow_add took 0.05...s
5

Lambdas: small anonymous functions#

A lambda is a function written as a single expression, without a name or def:

Python
square = lambda x: x * x         # works, but prefer def for named functions
print(square(7))

add = lambda a, b=10: a + b
print(add(5), add(5, 1))
Output
49
15 6

The syntax is lambda parameters: expression. The expression's value is returned automatically; there's no return, and no statements such as loops or assignments are allowed.

PEP 8 recommends not assigning lambdas to names — use def for that, since it gives better tracebacks and room for a docstring. Lambdas are meant to be passed straight into another function.

Where lambdas shine: key= functions#

Many built-ins accept a key function that says what to compare: sorted, min, max, list.sort, itertools.groupby, and more.

Python
products = [
    {"name": "Laptop", "price": 55000, "rating": 4.5},
    {"name": "Phone", "price": 18000, "rating": 4.7},
    {"name": "Tablet", "price": 25000, "rating": 4.1},
]

by_price = sorted(products, key=lambda p: p["price"])
print([p["name"] for p in by_price])

best = max(products, key=lambda p: p["rating"])
print(best["name"])

# sort by rating (highest first), then by price (lowest first)
ranked = sorted(products, key=lambda p: (-p["rating"], p["price"]))
print([p["name"] for p in ranked])
Output
['Phone', 'Tablet', 'Laptop']
Phone
['Phone', 'Laptop', 'Tablet']

Returning a tuple from the key sorts by several criteria at once; negating a number reverses its direction.

The operator module offers ready-made alternatives that are slightly faster and often clearer:

Python
from operator import itemgetter, attrgetter

pairs = [("b", 2), ("a", 3), ("c", 1)]
print(sorted(pairs, key=itemgetter(1)))
Output
[('c', 1), ('b', 2), ('a', 3)]

map() and filter()#

map(func, iterable) applies a function to every item; filter(func, iterable) keeps items where the function returns something truthy. Both return lazy iterators:

Python
nums = [1, 2, 3, 4, 5, 6]
print(list(map(lambda n: n * n, nums)))
print(list(filter(lambda n: n % 2 == 0, nums)))
print(list(map(str.upper, ["a", "b"])))      # any function works, not only lambdas
Output
[1, 4, 9, 16, 25, 36]
[2, 4, 6]
['A', 'B']

In modern Python, a comprehension is usually more readable than map/filter with a lambda: [n * n for n in nums] and [n for n in nums if n % 2 == 0]. That's the next lesson. functools.reduce also exists for folding a sequence into one value, but sum, max, min, any, all or a plain loop are usually clearer.

Worked example: a flexible HTML tag builder#

Python
def tag(name, *children, **attrs):
    attr_text = "".join(f' {k.rstrip("_")}="{v}"' for k, v in attrs.items())
    inner = "".join(children)
    return f"<{name}{attr_text}>{inner}</{name}>"


link = tag("a", "Elephantoo", href="https://elephantoo.com", class_="brand")
print(tag("p", "Learn Python at ", link, "!"))
Output
<p>Learn Python at <a href="https://elephantoo.com" class="brand">Elephantoo</a>!</p>

class is a Python keyword, so the example uses the common convention of a trailing underscore (class_) and strips it.

Common mistakes#

  • Forgetting to unpack when forwarding: func(args, kwargs) passes a tuple and a dict as two arguments. Write func(*args, **kwargs).
  • Overusing *args/**kwargs: explicit parameters document themselves and let editors help you. Use the flexible forms when you genuinely need them.
  • Complex lambdas: if it needs a comment, make it a named def.
  • Lambdas in loops capturing a loop variable — remember the late-binding gotcha from the scope lesson.

What's next#

Comprehensions came up twice in this lesson. Next you'll master them properly: list, dict and set comprehensions and generator expressions.

Check your understanding

Quick quiz

0/3 answered
  1. 1.Inside def f(*args, **kwargs), what type is kwargs?

  2. 2.What does f(*[1, 2, 3]) do?

  3. 3.Which statement about lambdas is true?

Finished reading?

Mark this lesson complete to track your progress.