Flexible arguments and functional tools
Design functions that accept variable inputs, pass behavior as a value, and choose readable transformations.
By the end, you’ll be able to…
- Use *args, **kwargs, and keyword-only parameters without losing a clear contract.
- Pass functions to sorted, map, and filter, and explain when a comprehension is clearer.
- Use any, all, and functools.reduce for small, well-understood folds.
01Collect arguments deliberately
A function signature is a public interface. *args collects extra positional arguments into a tuple, while **kwargs collects extra keyword arguments into a dictionary. Parameters after a bare * are keyword-only, which is useful for options that should be named at the call site. Python also supports positional-only parameters before a /, but use that boundary only when the parameter name is not part of the contract.
Flexibility is not the same as vagueness. Keep the important inputs explicit, give options meaningful defaults, and validate the values that remain flexible. A function that accepts **options but silently ignores unknown keys is harder to debug than one that documents and checks the supported set.
def summarize(title, *scores, passing=60): if not scores: return f"{title}: no scores" average = sum(scores) / len(scores) status = "pass" if average >= passing else "review" return f"{title}: {average:.1f} ({status})"print(summarize("Ada", 82, 91, 76))print(summarize("Lin", 54, 61, passing=70))Ada: 83.0 (pass) Lin: 57.5 (review)
02A function can be passed to another function
Functions are objects. You can store one in a variable, return it from another function, or pass it as an argument. The key parameter of sorted is a function that receives one item and returns the value to sort by. A lambda is a small anonymous function expression; it is convenient for a one-line key, but a named function is easier to test and reuse when the rule has a name.
The same idea powers max, min, and many library APIs. Keep key functions free of side effects: sorting may call a key function once per item, and the caller expects a stable answer. When a sort uses several fields, return a tuple from the key. Python's sort is stable, so sorting by a secondary field first and then by the primary field also works.
records = [("Ada", 92), ("Lin", 81), ("Sam", 57)]ranked = sorted(records, key=lambda item: item[1], reverse=True)print(ranked)print([name for name, score in ranked if score >= 60])[('Ada', 92), ('Lin', 81), ('Sam', 57)]
['Ada', 'Lin']03Map, filter, and comprehensions
map applies a function to every item and returns a lazy iterator. filter keeps items for which a predicate is true. Both are useful when the function already has a name, such as str.strip or math.sqrt. A list comprehension is often more readable when you need a simple expression and a condition in the same place. zip pairs items from parallel iterables, and enumerate supplies a position while you iterate.
functools.reduce folds a sequence into one value by repeatedly combining an accumulator with the next item. Use it for genuinely associative operations such as a product or a sum of structured values, and pass an explicit initial value when the sequence may be empty. any and all are clearer than writing a loop that returns a boolean. Two compact forms also belong in your vocabulary: a conditional expression such as `status = "pass" if score >= 60 else "review"` chooses one of two values, and the walrus operator (`if (count := len(items)) > 0:`) assigns a name inside a larger expression. Use both in small doses; nested conditional expressions and assignments hidden inside complicated conditions make code harder to read.
from functools import reduceraw = [" 8 ", "", "12", "oops"]cleaned = map(str.strip, filter(None, raw))print(list(cleaned))values = [2, 3, 4]print(reduce(lambda left, right: left * right, values, 1))names = ["Ada", "Lin"]scores = [92, 81]print(list(zip(names, scores)))for position, name in enumerate(names, start=1): print(position, name)print(any(value > 3 for value in values))print(all(value > 0 for value in values))['8', '12', 'oops']
24
[('Ada', 92), ('Lin', 81)]
1 Ada
2 Lin
True
True04Keep the interface readable
A flexible helper should make the common call short and the unusual call explicit. Prefer a small number of named options over a large **kwargs bag. If a function accepts a callback, document when it is called, what arguments it receives, and whether it may raise. Those details are part of the contract even when the callback is supplied by the caller.
Functional style is not a requirement. A for loop that updates a result is often the clearest choice, especially when it includes logging, validation, or an early exit. Choose the smallest construct that makes the data flow obvious. The goal is a function another person can call correctly without reading its implementation.
def tag(*parts, sep=" · "): clean = [part.strip() for part in parts if part.strip()] return sep.join(clean) or "(untagged)"print(tag(" python ", "", "cli", sep="/"))print(tag())python/cli (untagged)
The ideas worth keeping.
- Use *args and **kwargs only when the call shape genuinely varies, and keep the important parameters explicit.
- Functions are values: sorted, map, filter, and callbacks become easier once you read them as behavior passed into another operation.
- Choose comprehensions for small inline transformations and named functions for reusable rules.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
A reusable label
Write make_label(*parts, sep=' / ') that strips each non-empty part and joins the results. Return '(empty)' when nothing remains. Demonstrate the default and a custom separator.
Explore a worked solution
def make_label(*parts, sep=" / "): values = [part.strip() for part in parts if part.strip()] return sep.join(values) or "(empty)"print(make_label(" data", "", "cleaning "))print(make_label("api", "v2", sep="-"))*parts collects the positional inputs into a tuple. Keyword-only sep keeps the separator from being mistaken for another label.
Sort by two fields
Given a list of dictionaries with name and score fields, return the names ordered by score descending and then by name ascending, including only scores of at least 60.
Explore a worked solution
people = [ {"name": "Ada", "score": 92}, {"name": "Lin", "score": 92}, {"name": "Sam", "score": 57},]ordered = sorted(people, key=lambda person: (-person["score"], person["name"]))print([person["name"] for person in ordered if person["score"] >= 60])The key returns a tuple: negating the score makes the descending order explicit, and the name breaks ties in ascending order.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
A reusable label
Write make_label(*parts, sep=' / ') that strips each non-empty part and joins the results. Return '(empty)' when nothing remains. Demonstrate the default and a custom separator.
make_label(" data" "cleaning ")
make_label("api" v2 sep="-")Keep exploring.
The official references behind this chapter, when you want a second angle or the full details.
One chapter closer.
Reading is a start. Trying it yourself makes the connection.