Imagine you have three different lists and you need the length of every item in each one. You write the loop once, copy it twice, and change the list name. Later you realise you also need to strip whitespace first, so you edit the first copy, forget the second, and now your results are inconsistent.
The real problem is not the loop — it is that the behaviour (what to do to each item) is welded to the mechanics (how to iterate). A higher-order function separates the two. It is any function that either accepts another function as an argument or returns a function as its result. Once you can pass behaviour around as data, you write the loop once and plug in the changing part.
flowchart LR A["main()"] -->|"pass len"| B["apply_to_all(fn, items)"] B --> C["fn(item) for each item"] C --> D["return results"] style B fill:#3776ab,color:#fff
Functions as arguments
Python treats functions as first-class values. You can store them in variables, append them to lists, and hand them to other functions exactly like integers or strings. The built-in sorted already exploits this: passing key=len tells sorted to use the len function to decide the order of every item.
You can write your own higher-order tools. A reusable apply_to_all accepts any function and any list, runs the function on every item, and returns the results. Inside apply_to_all, the parameter fn is just a name that happens to hold a callable; you invoke it with parentheses exactly like any other function.
def apply_to_all(fn, items):
return [fn(item) for item in items]
words = ['cat', 'elephant', 'ant']
print(apply_to_all(len, words))
print(apply_to_all(lambda s: s.upper(), words))
The value-versus-call trap
Notice that len is written without parentheses. len is the function object itself; len() would call it immediately and pass the result (a number) instead of the function. That distinction — value versus call — is the first place higher-order code goes wrong.
The same trap appears when you store functions in a list. handlers = [print, len] stores two function objects. handlers = [print(), len()] would call them immediately and store their results (None and a number). If you later try to iterate over handlers and call each one, the second version explodes because integers are not callable.
flowchart TD A["make_multiplier(3)"] --> B["builds multiplier(x)"] B --> C["returns the function"] C --> D["triple = make_multiplier(3)"] D --> E["triple(4) -> 12"] style B fill:#3776ab,color:#fff
Functions as return values
A function can also build and hand back another function. This is sometimes called a factory pattern: the outer function configures the inner one, and the inner one remembers that configuration every time it is called later.
In the example below, make_multiplier(n) returns a new function that multiplies its argument by n. The returned function is a perfectly ordinary value — you can assign it to a name, pass it along, or call it immediately. Each factory call creates a fresh, independent function with its own captured value of n.
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
triple = make_multiplier(3)
print(triple(4))
print(triple(10))
double = make_multiplier(2)
print(double(5))
Why this matters
Higher-order functions let you separate what to do from how to iterate. The looping logic lives in one place; the changing behaviour arrives as a function argument. That means less duplicated code, fewer off-by-one errors, and tests that target small, pure functions instead of sprawling loops.
You have already met one higher-order function in disguise: the comprehension. [fn(x) for x in items] is a loop that accepts the transformation as an expression. Passing a real function is the generalisation of that idea. The difference is that a comprehension is limited to expressions; a higher-order function can accept any callable, including named functions with docstrings and complex logic.
flowchart LR A["loop logic"] --> B["higher-order function"] C["behaviour: fn"] --> B B --> D["clean separation"] style B fill:#3776ab,color:#fff
Storing and dispatching functions
Because functions are values, you can store them in dictionaries for simple dispatch tables. Instead of a long if...elif chain that checks a string and runs the right block, you map strings directly to functions and look them up. This scales better than conditional chains, and adding a new operation means adding one dictionary entry instead of editing a branching structure.
ops = {
'double': lambda x: x * 2,
'negate': lambda x: -x,
}
result = ops['double'](5) # 10
This pattern is the seed of more advanced designs like command patterns and plugin architectures. The higher-order idea — passing behaviour as data — is what makes them possible.
def double(x):
return x * 2
def negate(x):
return -x
ops = {'double': double, 'negate': negate}
print(ops['double'](5))
print(ops['negate'](3))
Write apply_to_all(fn, items) that returns a new list containing fn(item) for every item in items. Use a list comprehension.
def apply_to_all(fn, items):
# your code here
pass
Loop over items and call fn(item) inside a list comprehension.
Remember to return the list, not print it.
def apply_to_all(fn, items):
return [fn(item) for item in items]
Read carefully. The variable shout holds a function; shout('hi') calls it.
def make_shout(word):
def shout():
return word.upper()
return shout
say_hi = make_shout('hi')
print(say_hi())
make_shout returns the inner shout function, not the result of calling it.
say_hi() then calls that inner function, which returns 'HI'.
Write make_adder(n) that returns a function which adds n to its argument. So add5 = make_adder(5) and add5(10) gives 15.
def make_adder(n):
# your code here
pass
Define an inner function that takes x and returns x + n.
Return that inner function, not the result of calling it.
def make_adder(n):
def adder(x):
return x + n
return adder
This factory is meant to return a function, but it calls the inner function immediately and returns the result (a number). Fix it so it returns the function itself.
def make_suffixer(suffix):
def suffixer(word):
return word + suffix
return suffixer('test')
Return suffixer, not suffixer('test').
The caller will supply the argument later.
def make_suffixer(suffix):
def suffixer(word):
return word + suffix
return suffixer
Write select_and_transform(items, test_fn, transform_fn) that keeps only items where test_fn(item) is truthy, then applies transform_fn to each kept item. Return the final list. Do this in one comprehension.
def select_and_transform(items, test_fn, transform_fn):
# one comprehension with an if clause
pass
Use [transform_fn(item) for item in items if test_fn(item)].
The if clause filters; the left side transforms.
def select_and_transform(items, test_fn, transform_fn):
return [transform_fn(item) for item in items if test_fn(item)]
Recap
- Functions are values: you can store them in variables, append them to lists, pass them as arguments, and return them from other functions.
- A higher-order function takes a function as an argument or returns one as a result.
- Pass the function object (
len) rather than calling it (len()). Calling too early is the most common beginner mistake. - Factory functions build configured functions and return them for later use; each call creates an independent closure.
- Function dispatch tables replace long
if...elifchains with lookups, making code easier to extend.
Next you will meet three classic higher-order tools — map, filter, and reduce — that turn common loop patterns into single expressions.