You've been sneaking up on a big idea for three lessons: sorted(key=len) passed a function, the timed() wrapper received one, .apply() will take one per column. Time to say it plainly — in Python, functions are values. Store them, pass them, return them. Code that operates on code.
What you'll learn
What
A higher-order function either takes a function as an argument (map, filter, sorted), returns a function as its result (a 'factory'), or both. Everything else is ordinary Python — functions are objects like any other value.
Why
Higher-order thinking is how data tooling is built: pandas' .apply and .agg take your functions; sklearn pipelines chain them; decorators wrap them. Understand functions-as-values and these stop being magic APIs and become plain patterns you could write yourself.
Where it's used
map/filter over records, .apply()/.agg() in pandas, validator registries, function factories for configurable transforms, and (later) decorators.
Where this runs in production
df.groupby('region').agg(total=('sales', sum), top=('sales', max)) — aggregation IS higher-order: you hand pandas the functions to run per group.
A Django site maps URL patterns to view FUNCTIONS stored in a list — dispatch is 'look up the function, call it', the registry pattern at web scale.
Ray parallelizes work by shipping your function to worker processes over a cluster — map() semantics stretched across machines, same mental model.
def shout(text):
return text.upper() + '!'
announce = shout # alias — no call
print(announce('deploy')) # DEPLOY!
print(shout is announce) # True — same object, two namesannounce isn't a copy; it's a second sticky note on the same function (the Variables lesson's whiteboard, paying off). This is what you did with key=len — handed sorted the len object itself.
A general contractor doesn't lay tile — they're hired WITH a specialist in hand: 'renovate this kitchen, use THIS tiler'. map() is such a contractor: it handles walking the rooms (iteration) while the function you hand it does the craft (transformation). Factories go one step further: a workshop that doesn't tile either — it TRAINS specialist tilers to order ('make me a tiler who only does 10cm blue tiles') and sends them out to work.
map(func, items) applies func to every item; filter(func, items) keeps items where func returns True. Both return lazy iterators — wrap in list() to see results. Honest Python style: comprehensions usually read better when you'd write a fresh lambda ([x*2 for x in xs] beats map(lambda x: x*2, xs)), while map/filter shine when the function ALREADY EXISTS: map(str.strip, lines), filter(is_valid, rows).
lines = [' alpha ', 'beta ', ' gamma'] clean = list(map(str.strip, lines)) print(clean) scores = [72, 45, 91, 58] passing = list(filter(lambda s: s >= 60, scores)) print(passing)
map with an existing function: no lambda, pure signal. filter with a fresh predicate: fine, though [s for s in scores if s >= 60] is equally good — know both, choose the readable one.
A factory is a def containing another def, returning the inner one. The inner function REMEMBERS the factory's arguments (the mechanics of that memory — closures — is next lesson's whole topic; today we use it). make_multiplier(3) returns a function that triples; make_validator(0, 120) returns an age-checker. One factory, a family of configured tools.
One: calling too early — map(clean(), rows) passes clean's RESULT (probably a crash or a string); map(clean, rows) passes clean itself. If you see 'TypeError: 'str' object is not callable', you called too early somewhere. Two: forgetting map/filter are lazy — print(map(...)) shows '<map object ...>', not data; list() it, or loop it, to realize the work.
Take a function, return a function, store functions — click each to see the pattern and where you'll meet it.
Apply an existing transform to a whole list with map.
to_kilometers — no parentheses — hands map the function itself. map calls it once per element; list() collects the lazy results.
[4.8, 42.2, 0.8]
Your task
An online store needs tiered discount functions. Write a factory make_discount(pct) returning a function that applies that percentage to a price (rounded to 2 decimals). Create student (10%) and vip (25%) discounters, then use map to apply the vip discount to a whole cart.
180.0 150.0 [37.49, 90.0, 11.62]
Write your solution in the editor on the right, then hit Run.
What makes a function 'higher-order'?
What does print(map(str.upper, ['a', 'b'])) show?
Which call is broken? def clean(s): return s.strip() rows = [' a ', ' b ']
What's the higher-order refactor?
Using filter and a named predicate is_round(n) (True when n is divisible by 10), keep the round numbers from [95, 100, 42, 30, 7, 60] and print them. Expected output: [100, 30, 60]
Write a factory make_suffixer(suffix) returning a function that appends suffix to a string. Build add_csv = make_suffixer('.csv'), then use map to convert ['sales', 'users'] into filenames. Print the list. Expected output: ['sales.csv', 'users.csv']
def make_adder(n): def add(x): return x + n return add add5 = make_adder(5) add9 = make_adder(9) print(add5(1), add9(1)) What prints?
What does 'functions are first-class values' mean in Python, and why does it matter for data work?
It means a function is an ordinary object: created by def, bound to a name, and usable anywhere a value is — assigned to other names, stored in lists and dicts, passed as arguments, returned from other functions. The name alone references the object; parentheses perform the separate act of calling. It matters for data work because the entire tooling surface assumes it: sorted(key=...), df['col'].apply(func), groupby().agg(func), sklearn pipelines of transformers, and decorators all receive YOUR function as a value and control when to call it. Once you see the APIs as 'they take my function', you can also invert it — write your own map-like utilities, registries, and pipelines instead of if/elif sprawl.
map/filter versus list comprehensions — how do you choose?
They're expressively equivalent for the common cases, so the choice is readability. My rule: when the function already exists, map/filter is cleaner — map(str.strip, lines) and filter(is_valid, rows) name the operation with zero ceremony. When I'd have to write a fresh lambda, the comprehension usually wins — [x * 2 for x in xs] over map(lambda x: x * 2, xs), and comprehensions handle transform-plus-filter in one expression where map-inside-filter nests awkwardly. Two technical notes worth adding: map and filter are lazy iterators — nothing runs until consumed, which is a feature for large streams and a gotcha when you print one expecting data — and at pandas scale the real answer becomes vectorized column operations, with .apply as the fallback.
Describe the function-factory pattern and a real situation where you'd use it.
A factory is a function that builds and returns another function, configured by the factory's arguments: def make_range_check(low, high) defines an inner check(value) using low and high, and returns it. Each factory call produces an independent function remembering its own configuration — the remembering is a closure over the factory's variables. Real uses: generating per-column validators from a config file (loop the config, build one checker per column, store them in a dict keyed by column name); building parameterized transforms for a cleaning pipeline; and rate-limiters or retry policies configured per external service. The pattern's payoff is that configuration becomes a VALUE you can pass around, test, and store — and it's the exact mechanism decorators are built from, which makes it a favorite interview stepping-stone question.
Common Mistakes to Avoid
1) Calling instead of passing: map(clean(), rows) runs clean immediately — drop the parentheses. 2) Printing a map/filter object and thinking it's broken — they're lazy; list() them. 3) Wrapping existing functions in lambdas (lambda s: s.strip() → just str.strip). 4) Factories that return inner() instead of inner — returning the CALL's result, not the function. 5) Registries without a missing-key plan — HANDLERS[kind] crashes on unknowns; .get() + explicit handling reports them. 6) Consuming a lazy iterator twice — the second pass is empty; realize to a list if you need it twice.
Ask the AI Tutor
Try these prompts in the AI Tutor panel: • 'Quiz me: which of these six expressions pass a function and which call it?' • 'Help me refactor this if/elif chain into a dispatch dict.' • 'Show me a factory that builds validators from a config list.' • 'Trace what map actually does, step by step, on three elements.' • 'Interview mode: ask me about first-class functions and grade my answer.'
Glossary
Higher-order function — takes and/or returns functions. First-class value — anything assignable, passable, and returnable; Python functions qualify. map(func, iterable) — lazily applies func to every element. filter(pred, iterable) — lazily keeps elements where pred is True. Predicate — a function returning True/False. Factory — a function that returns a newly built, configured function. Registry / dispatch table — a dict mapping keys to handler functions. Lazy iterator — computes values on demand; consumed once. Alias — a second name bound to the same function object. Pipeline — an ordered collection of functions applied in sequence.
Recommended Resources
• Docs: the built-ins reference for map() and filter(), plus 'Functional Programming HOWTO' for the wider toolkit. • Read: sklearn's Pipeline documentation intro — recognize the list-of-steps pattern you just built by hand. • Practice: refactor any if/elif chain from your earlier code into a dispatch dict with a .get() fallback. • Next in DSM: factories work because inner functions REMEMBER outer variables — Scope & Closures explains exactly where Python looks names up, and what that memory really is.
Recap
✓ Functions are values: the name references, the parentheses call — everything follows from that. ✓ map/filter apply your function across data lazily; comprehensions are their (often clearer) twins. ✓ Prefer map/filter when the function exists; comprehensions when you'd write a lambda. ✓ Factories return configured functions — configuration as a value. ✓ Functions in dicts/lists give dispatch tables and pipelines — extension by data, not by elif. ✓ Two slips to watch: calling too early, and trusting a lazy iterator you never realized. Next up: Scope & Closures. Factories left one mystery — how does the inner function remember the factory's variables after it returns? The LEGB rule and closures are the answer.
Run your code to see the output here.