Higher-Order Functions
Reviewed & published by Brayan K
Master the core functional programming techniques that power Python frameworks, AI pipelines, backend logic, decorators, and more.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- • What higher-order functions are and why they matter
- • Passing functions as arguments and returning them as values
- • Built-in HOFs: map(), filter(), sorted()
- • Building function factories and closures
- • Composing pipelines from reusable functional building blocks
This lesson takes you from "I know what functions are" → "I can write dynamic, scalable, factory-generated functions used in real systems."
Higher-order functions (HOFs) and function factories are two of the most important advanced concepts in Python — and they unlock a completely new style of coding.
🔥 1. What Are Higher-Order Functions?
🏠 Real-World Analogy:
Think of a coffee machine. You can give it different "programs" (espresso, latte, cappuccino) and it executes them. A higher-order function is like that machine—it takes a "program" (function) as input and runs it!
| HOF Type | What It Does | Example |
|---|---|---|
| Takes function as argument | Receives behavior to execute | map(func, list) |
| Returns a function | Creates new behavior dynamically | def factory(): return inner |
| Both! | Takes function, returns modified version | @decorator |
def apply_twice(fn, value):
# This function TAKES another function as an argument!
return fn(fn(value)) # Call fn twice
def add_one(x):
return x + 1
# We pass add_one INTO apply_twice
result = apply_twice(add_one, 5)
print(result) # 7 → add_one(add_one(5)) = add_one(6) = 7
# ✅ Expected output:
# 7- ✔ Decorators (modify function behavior)
- ✔ Event systems (callbacks)
- ✔ Middleware (request/response processing)
- ✔ Functional pipelines (data transformations)
🧠 2. Functions as Variables
A function in Python is just an object:
def greet(name):
return f"Hello, {name}!"
say_hello = greet
print(say_hello("Alex"))
# ✅ Expected output:
# Hello, Alex!- ✔ plug-and-play behaviors
- ✔ dynamic function swapping
- ✔ architectures where behavior is controlled by configuration
⚙️ 3. Passing Functions as Arguments
Example: a custom map implementation.
def apply_each(fn, values):
return [fn(v) for v in values]
print(apply_each(lambda x: x * 2, [1, 2, 3]))
# ✅ Expected output:
# [2, 4, 6]- Machine learning transforms
- Web scraping pipelines
- Backends that process requests
🧪 3b. Worked Example — The Built-In Higher-Order Functions
You have been writing your own higher-order functions. Python ships with four you will reach for constantly — map, filter, sorted(key=...) and functools.reduce. Every one of them takes a function as an argument: you supply the behaviour, they handle the looping.
Read the comments before running. Each says what the line produces and when you would choose it.
from functools import reduce
prices = [12.5, 4.0, 30.0, 7.25, 99.99]
# ---- map: run one function over every item ---------------------------------
# map is lazy: it returns a map object, not a list, so wrap it in list() to see
# the values. Forgetting that is the #1 map surprise.
with_vat = list(map(lambda p: round(p * 1.2, 2), prices))
print(with_vat)
# ---- filter: keep the items where the function returns True ----------------
def is_cheap(price):
return price < 20 # a named function beats a long lambda for clarity
print(list(filter(is_cheap, prices)))
# ---- sorted(key=...): sort by a computed value -----------------------------
# The key function runs once per item; sorted then orders by what it returned.
words = ["banana", "fig", "cherry", "kiwi"]
print(sorted(words, key=len)) # shortest first
print(sorted(words, key=len, reverse=True)) # longest first; ties keep their order
# key earns its keep on lists of dicts:
people = [{"name": "Ada", "age": 36}, {"name": "Alan", "age": 41}, {"name": "Grace", "age": 45}]
oldest_first = sorted(people, key=lambda person: person["age"], reverse=True)
print([p["name"] for p in oldest_first])
# ---- reduce: fold a whole list down to ONE value ---------------------------
# It carries a running result: ((12.5 + 4.0) + 30.0) + 7.25 + 99.99
total = reduce(lambda running, price: running + price, prices)
print(round(total, 2))
# For adding you would just use sum(). reduce is for the folds with no built-in,
# like multiplying every item together:
print(reduce(lambda a, b: a * b, [1, 2, 3, 4]))
# ---- a function IS a value ---------------------------------------------------
def shout(text):
return text.upper() + "!"
def whisper(text):
return text.lower() + "..."
for style in (shout, whisper): # a tuple of FUNCTIONS — note: no parentheses
print(style("Higher order"))
# ✅ Expected output:
# [15.0, 4.8, 36.0, 8.7, 119.99]
# [12.5, 4.0, 7.25]
# ['fig', 'kiwi', 'banana', 'cherry']
# ['banana', 'cherry', 'kiwi', 'fig']
# ['Grace', 'Alan', 'Ada']
# 153.74
# 24
# HIGHER ORDER!
# higher order...Look closely at the reverse sort: banana still comes before cherry even though both are six letters long. Python's sort is stable — items with equal keys keep the order they were already in, which is what lets you sort by one field and then another to get a two-level ordering.
🎯 3c. Your Turn — Pick the Right Built-In
Four blanks. Each one is a name from the worked example above — nothing new. Run it and check the expected output at the bottom.
# 🎯 YOUR TURN — use the built-in higher-order functions
# Fill in the four blanks marked ___
scores = [55, 91, 42, 78, 100]
# 👉 blank 1: the HOF that runs a function over EVERY item
doubled = list(___(lambda s: s * 2, scores))
print(doubled)
# 👉 blank 2: the HOF that KEEPS the items where the function returns True
passed = list(___(lambda s: s >= 60, scores))
print(passed)
names = ["ada", "grace", "alan", "bob"]
# 👉 blank 3: the argument that tells sorted() what to sort BY
print(sorted(names, ___=len))
def initial(word):
return word[0]
# 👉 blank 4: hand over the function itself — its name, with no parentheses
print(sorted(names, key=___))
# ✅ Expected output:
# [110, 182, 84, 156, 200]
# [91, 78, 100]
# ['ada', 'bob', 'alan', 'grace']
# ['ada', 'alan', 'bob', 'grace']If blank 4 gives you TypeError: 'str' object is not callable, you wrote key=initial("ada"). sorted wants the function, not one answer from it — it will do the calling itself, once per name.
🔁 4. Returning Functions (First Big Step Toward Factories)
Imagine a cookie cutter factory. You tell the factory what shape you want (star, heart, circle), and it gives you a custom cookie cutter. Each cutter is different, but they all came from the same factory!
| Step | What Happens |
|---|---|
| 1. Call outer function | power(2) → saves exponent=2 |
| 2. Inner function is created | inner remembers exponent=2 |
| 3. Return inner function | You get a new function back! |
| 4. Use the new function | square(5) → 5² = 25 |
def power(exponent):
# This outer function "configures" the inner function
def inner(base):
return base ** exponent # Uses exponent from outer scope!
return inner # Return the inner function itself
# Create specialized functions
square = power(2) # square remembers exponent=2
cube = power(3) # cube remembers exponent=3
# Now use them!
print(square(5)) # 25 (5²)
print(cube(5)) # 125 (5³)
print(power(4)(2)) # 16 (2⁴) - can also use directly!
# ✅ Expected output:
# 25
# 125
# 16This pattern is seen in:
- ✔ Custom ML metrics (create metric with specific threshold)
- ✔ Validators (create validator with specific rules)
- ✔ Loggers (create logger with specific prefix)
- ✔ Rate limiters (create limiter with specific limits)
🔒 5. Closures (The Foundation of Function Factories)
A closure is like a backpack. When you create an inner function, it packs any variables it needs from the outer function into its backpack. Even after the outer function finishes, the inner function still has its backpack!
| Closure Ingredient | What It Means |
|---|---|
| 1. Nested function | A function defined inside another function |
| 2. Uses outer variable | The inner function references a variable from the outer function |
| 3. Returned/passed out | The inner function is returned or used outside |
| 4. Remembers! | The variable is "captured" and remembered forever |
def make_counter():
count = 0 # This variable gets "captured" by the closure
def inc():
nonlocal count # "I want to MODIFY the outer variable"
count += 1
return count
return inc # Return the inner function with its "backpack"
# Create two separate counters
counter1 = make_counter()
counter2 = make_counter()
print(counter1()) # 1 - counter1's count
print(counter1()) # 2 - counter1's count
print(counter2()) # 1 - counter2 has its OWN count!
print(counter1()) # 3 - counter1 continues where it left off
# ✅ Expected output:
# 1
# 2
# 1
# 3💡 Closures = data + behavior bundled elegantly without classes. They're like lightweight objects!
🧬 6. Function Factories (Dynamic Function Generators)
Function factories = functions that create functions.
Example 1: A configurable logger factory
def logger(prefix):
def log(message):
print(f"[{prefix}] {message}")
return log
info = logger("INFO")
warn = logger("WARNING")
info("Server started")
warn("CPU usage high")
# ✅ Expected output:
# [INFO] Server started
# [WARNING] CPU usage highExample 2: Validation function factory
def min_length(n):
def validate(value):
return len(value) >= n
return validate
validate_password = min_length(8)
print(validate_password("hello123")) # True
# ✅ Expected output:
# TrueThese factories are used everywhere:
- Django & Flask forms
- FastAPI validators
- Pandas transformations
- AI feature engineering
- Security rules
- Permission systems
🧱 7. Combining HOF + Closures = Decorators
🤔 How Decorators Work:
A decorator is simply a function that: (1) takes a function as input, (2) creates a wrapper function inside, (3) returns the wrapper. The wrapper "decorates" the original function with extra behavior!
| Decorator Part | HOF/Closure Concept |
|---|---|
| def track(fn): | HOF: takes a function as argument |
| def wrapper(): | Closure: defined inside, uses fn |
| return wrapper | HOF: returns a function |
| @track | Syntactic sugar for greet = track(greet) |
def track(fn): # Step 1: Takes a function
def wrapper(*args, **kwargs): # Step 2: Create wrapper (closure!)
print("Calling:", fn.__name__) # Extra behavior BEFORE
result = fn(*args, **kwargs) # Call original function
print("Done!") # Extra behavior AFTER
return result
return wrapper # Step 3: Return the wrapper
@track # This is the same as: greet = track(greet)
def greet():
print("Hello!")
greet() # Now greet is actually wrapper!
# ✅ Expected output:
# Calling: greet
# Hello!
# Done!🔄 8. Function Composition (Advanced HOF Technique)
Think of an assembly line. Raw material goes in, passes through multiple stations (cutting → painting → packaging), and a finished product comes out. Each station is a function!
| Without Composition | With Composition |
|---|---|
| double(add_one(3)) | combined(3) |
| Nested calls, hard to read | Single call, reusable pipeline |
def compose(f, g):
# Returns a NEW function that applies g first, then f
return lambda x: f(g(x))
def double(n): return n * 2
def add_one(n): return n + 1
# Create a pipeline: first add_one, then double
combined = compose(double, add_one)
# compose(f, g) means: f(g(x))
# So combined(3) = double(add_one(3)) = double(4) = 8
print(combined(3)) # 8
# You can chain more!
triple = lambda x: x * 3
mega = compose(triple, combined) # triple(double(add_one(x)))
print(mega(3)) # triple(8) = 24
# ✅ Expected output:
# 8
# 24This is the basis of:
- ✔ ML pipelines (clean → normalize → encode → train)
- ✔ Data engineering (extract → transform → load)
- ✔ Text processing (strip → lowercase → remove punctuation)
⚡ 9. Real Engineering Example — Rate Limiter Factory
A powerful real-world example of HOF + closure:
import time
def rate_limiter(max_calls, per_seconds):
calls = []
def decorator(fn):
def wrapper(*args, **kwargs):
nonlocal calls
now = time.time()
calls = [t for t in calls if now - t < per_seconds]
if len(calls) >= max_calls:
raise Exception("Rate limit exceeded")
calls.append(now)
return fn(*args, **kwargs)
return wrapper
return decorator
@rate_limiter(3, 5)
def fetch():
print("Request sent!")
for _ in range(4):
try:
fetch()
except Exception as e:
print(e)This pattern is literally used in:
- API gatekeepers
- Discord/Telegram bots
- Payment systems (Stripe throttling)
- Cloud server wrappers
- Security middleware
🚀 10. Real Engineering Example — Machine Learning Preprocessing Factory
def scaler(min_value, max_value):
def scale(x):
return (x - min_value) / (max_value - min_value)
return scale
normalize = scaler(0, 255)
print(normalize(128)) # 0.5019...
# ✅ Expected output:
# 0.5019607843137255- image preprocessing
- audio normalization
- numerical feature scaling
- AI model data pipelines
🧠 11. Real Engineering Example — Query Builder Factory
def query_builder(table):
def where(**conditions):
clause = " AND ".join(f"{k}='{v}'" for k, v in conditions.items())
return f"SELECT * FROM {table} WHERE {clause}"
return where
users_query = query_builder("users")
print(users_query(name="Alex", age=20))
# ✅ Expected output:
# SELECT * FROM users WHERE name='Alex' AND age='20'- backend frameworks
- SQL query engines
- Data dashboards
🎓 Conclusion
By mastering higher-order functions and function factories, you now understand:
- ✔ Functions as first-class objects
- ✔ Passing functions as arguments
- ✔ Returning functions
- ✔ Dynamic function generation
- ✔ Function composition
- ✔ The architecture behind decorators
- ✔ Real engineering examples
These concepts fuel Python's most powerful systems and make your code:
- 🔥 more reusable
- 🔥 more flexible
- 🔥 more scalable
- 🔥 more professional
🎯 Mini-Challenge: Build a Checkout Pipeline
This is the whole lesson in one exercise: a function that takes functions and returns a function. pipeline(...) should accept any number of one-argument functions and hand back a single new function that runs them in order, left to right. Only the outline is given.
# 🎯 MINI-CHALLENGE: a discount pipeline built out of functions
#
# 1. Write three tiny functions, each taking a price and returning a price:
# remove_vat(p) -> p / 1.2
# apply_discount(p) -> p * 0.9
# to_pennies(p) -> round(p * 100)
#
# 2. Write the higher-order function:
# def pipeline(*functions):
# # *functions collects them all into a tuple
# # return a NEW function that takes one value and feeds it through
# # every function in turn, left to right, returning the final result
#
# 3. checkout = pipeline(remove_vat, apply_discount, to_pennies)
#
# 4. print(checkout(120.0))
# print([checkout(p) for p in [120.0, 60.0, 12.0]])
#
# ✅ Expected output:
# 9000
# [9000, 4500, 900]
# your code hereThe trap: pipeline must return the inner function, not call it. If checkout(120.0) raises TypeError: 'NoneType' object is not callable, the return at the end of pipeline is missing.
📋 Quick Reference — Higher-Order Functions
| Syntax | What it does |
|---|---|
| map(fn, lst) | Apply fn to each item in a list |
| filter(fn, lst) | Keep items where fn returns True |
| functools.reduce(fn, lst) | Accumulate list down to one value |
| sorted(lst, key=fn) | Sort by a custom key function |
| fn = lambda x: x + 1 | Create a small inline function |
🏆 Lesson Complete!
You now understand how to pass, return, and compose functions — the foundation of clean, reusable Python code.
Practice quiz
What is a higher-order function?
- A function with many parameters
- A function defined at the top of a file
- A function that takes or returns another function
- A recursive function
Answer: A function that takes or returns another function. A higher-order function takes a function as an argument and/or returns a function.
When passing a function to another, why write add_one and not add_one()?
- add_one passes the function itself; add_one() calls it first
- They are the same
- add_one() is a syntax error
- add_one returns None
Answer: add_one passes the function itself; add_one() calls it first. Without parentheses you pass the function object; with them you pass its return value.
Given apply_twice(fn, value) returning fn(fn(value)) and add_one(x)=x+1, what is apply_twice(add_one, 5)?
- 6
- 10
- 5
- 7
Answer: 7. add_one(add_one(5)) = add_one(6) = 7.
Given power(exponent) returning inner(base)=base**exponent, what does power(2)(5) return?
- 10
- 25
- 7
- 32
Answer: 25. exponent=2 is captured, so inner(5) returns 5**2 = 25.
What does power(4)(2) return for that same factory?
- 16
- 8
- 6
- 64
Answer: 16. exponent=4, so 2**4 = 16.
Given compose(f, g) returning f(g(x)), with double(n)=n*2 and add_one(n)=n+1, what does compose(double, add_one)(3) return?
- 7
- 9
- 8
- 6
Answer: 8. g runs first: add_one(3)=4, then double(4)=8.
In compose(f, g) returning f(g(x)), which function runs first?
- f
- g
- Both at once
- Neither
Answer: g. Read right-to-left: g runs first, then f is applied to its result.
What is a function factory?
- A class that builds objects
- A built-in module
- A decorator library
- A function that creates and returns other functions
Answer: A function that creates and returns other functions. A function factory is a function that builds and returns customized functions.
Which keyword does the make_counter closure need to MODIFY its captured count?
- global
- nonlocal
- yield
- lambda
Answer: nonlocal. nonlocal lets the inner function change the outer function's count variable.
The decorator syntax @track applied to greet is shorthand for what?
- greet = track
- track = greet(track)
- greet = track(greet)
- greet()
Answer: greet = track(greet). @track is syntactic sugar for greet = track(greet).
Continue this course
- Previous: Advanced Functions & Parameters Masterclass
- Next: Closures & Lexical Scope in Real Projects — Understand variable capture and build stateful function patterns
- Quick reference: Python cheat sheet
- From the blog: Python Decorators: A Practical Guide