This guide covers each one in detail, verified by running the actual code, with real examples from Airbnb, Spotify, Amazon, X (formerly Twitter), Reddit, and AWS.
enumerate(): Numbering an Iterable
Looping over a list normally only gives you the values, not their positions:
numbers = [10, 20, 45, 8, 23, 32, 90, 11]
for value in numbers:
print(value)
If you also need to know where each value sits in the list, enumerate() wraps the iterable and hands back a running counter alongside each item:
for counter, value in enumerate(numbers):
print(counter, value)
0 10
1 20
2 45
3 8
4 23
5 32
6 90
7 11
The counter defaults to starting at 0, but you can tell it to start anywhere:
for counter, value in enumerate(numbers, 5):
print(counter, value)
5 10
6 20
7 45
8 8
9 23
10 32
11 90
12 11
That second argument to enumerate() is genuinely useful anytime you need "position 1" to mean the first item instead of the zeroth, like displaying a numbered list to a user.
zip(): Pairing Up Multiple Sequences
zip() takes two or more iterables and pairs up their items position by position, stopping as soon as the shortest one runs out:
names = ["Vinay", "Ronit", "Sachin", "Surya", "Virat"]
passwords = (123, 234, 345, 456, 567)
age = {21, 22, 23, 24, 25}
zipped_data = list(zip(names, passwords, age))
[('Vinay', 123, 21), ('Ronit', 234, 22), ('Sachin', 345, 23), ('Surya', 456, 24), ('Virat', 567, 25)]
This particular example mixes three different container types, a list, a tuple, and a set, and it happens to line up cleanly here. Worth knowing why "happens to" is doing real work in that sentence: a set never guarantees any particular order. The reason {21, 22, 23, 24, 25} comes out sorted above is a detail of how CPython currently hashes small integers, not a promise the language makes. Swap in a set of strings and the illusion breaks immediately:
sample = {'twenty-one', 'twenty-two', 'twenty-three', 'twenty-four', 'twenty-five'}
list(sample)
['twenty-one', 'twenty-two', 'twenty-five', 'twenty-three', 'twenty-four']
Whenever the position of an item matters, which is the entire point of zip(), stick to ordered containers (lists or tuples) for every sequence involved. A set is the right tool when you only care about membership and uniqueness, not the tool for anything where "3rd item" needs to mean something reliable.
filter(), map(), and reduce(): The Big Three
These three show up constantly once you start writing more compact code, and they're easiest to understand side by side, since they all take a function and an iterable but do three genuinely different things with them.
filter(function, iterable)keeps only the items wherefunctionreturns something truthy. The output is shorter than or equal to the input.map(function, iterable)transforms every item throughfunction. The output is always the same length as the input.reduce(function, iterable)collapses the whole iterable down to a single value, by repeatedly combining items two at a time.
filter()
def find_positive(num):
if num > 0:
return num
list1 = list(range(-10, 11))
positive_values = list(filter(find_positive, list1))
positive_values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
This gets the right answer, but it's worth noticing exactly why: find_positive never actually returns True or False. For a positive number it returns the number itself; for anything else, since there's no else, it falls through and implicitly returns None. filter() doesn't require an actual boolean, just something Python considers truthy or falsy, and both a positive number and None fit that bill correctly here. It works, but a function written to explicitly return num > 0 says the same thing more clearly and doesn't rely on a reader knowing Python's truthiness rules. The Airbnb example below does exactly that.
Airbnb: Filtering Search Results. A search returns thousands of listings; the site only needs the ones under a price cutoff.
listings = [
{"title": "Cozy Studio", "price": 95},
{"title": "Luxury Villa", "price": 450},
{"title": "Downtown Loft", "price": 130},
{"title": "Beachfront Condo", "price": 200}
]
def is_affordable(listing):
return listing["price"] < 150
affordable_places = list(filter(is_affordable, listings))
for place in affordable_places:
print(f"Found: {place['title']} - ${place['price']}/night")
Found: Cozy Studio - $95/night
Found: Downtown Loft - $130/night
map()
def square(num):
return num ** 2
list2 = [2, 65, 32, 11, 7, 8]
mapped_values = list(map(square, list2))
mapped_values = [4, 4225, 1024, 121, 49, 64]
Spotify: Currency Conversion for Royalties. Every value in an earnings list needs the same conversion applied to it, which is exactly what map() is for.
usd_earnings = [100.50, 250.00, 45.75, 800.20]
exchange_rate = 0.92 # 1 USD = 0.92 EUR
def convert_to_euros(amount):
return round(amount * exchange_rate, 2)
eur_earnings = list(map(convert_to_euros, usd_earnings))
print(f"Original USD: {usd_earnings}")
print(f"Converted EUR: {eur_earnings}")
Original USD: [100.5, 250.0, 45.75, 800.2]
Converted EUR: [92.46, 230.0, 42.09, 736.18]
reduce()
Unlike filter and map, reduce isn't a built-in you can call directly; it lives in the functools module. Here's the plain loop it replaces, side by side with the function itself:
# The manual version
product = 1
lst = [1, 2, 3, 4]
for num in lst:
product *= num
print(product) # 24
from functools import reduce
def multiply(x, y):
return x * y
list3 = [1, 2, 3, 4, 5]
product = reduce(multiply, list3) # 120
reduce calls multiply on the first two items, then calls it again on that result and the third item, and so on, carrying a running total forward until only one value is left.
Amazon Logistics: Total Freight Weight. A cargo plane's total load weight is exactly this kind of rolling sum.
from functools import reduce
package_weights = [12.5, 4.0, 8.2, 1.5, 20.0]
def add_weights(weight1, weight2):
return weight1 + weight2
total_cargo_weight = reduce(add_weights, package_weights)
print(f"Individual packages: {package_weights}")
print(f"Total takeoff weight: {total_cargo_weight} kg")
Individual packages: [12.5, 4.0, 8.2, 1.5, 20.0]
Total takeoff weight: 46.2 kg
Anonymous Functions: lambda
A lambda is a function without a name, written on a single line. The rules are a little different from a normal def:
- It can take any number of parameters, or none.
lambdareplacesdef functionname(...); no parentheses go around the parameter list.- A colon separates the parameters from the expression.
- There's no
returnkeyword; the expression's value is automatically what gets sent back.
Syntax: lambda parameters: expression
A normal function and its lambda equivalent, side by side:
def addition(num1, num2):
return num1 + num2
lambda num1, num2: num1 + num2
A slightly more involved one, including a conditional expression inside the lambda:
def maximum(num1, num2):
if num1 > num2:
return num1
else:
return num2
maximum = lambda num1, num2: num1 if num1 > num2 else num2
maximum(23, 45) # 45
Lambdas are used constantly alongside filter, map, and reduce, specifically because writing a full def for a one-line throwaway function is more ceremony than the logic deserves:
lst = [1, 2, 3, 4, 5]
even_lst = list(filter(lambda num: num % 2 == 0, lst))
print(even_lst) # [2, 4]
new_lst = list(map(lambda x: x ** 2, lst))
print(new_lst) # [1, 4, 9, 16, 25]
from functools import reduce
product_lst = reduce(lambda x, y: x * y, lst)
print(product_lst) # 120
X (Twitter): Sorting Trending Topics. sorted() accepts a key function that tells it what to compare, and a lambda is the natural fit for "just look at this one field."
trending_topics = [
{"hashtag": "#Python", "volume": 15000},
{"hashtag": "#TechNews", "volume": 8500},
{"hashtag": "#Coding", "volume": 22000},
{"hashtag": "#AI", "volume": 45000}
]
sorted_trends = sorted(trending_topics, key=lambda topic: topic["volume"], reverse=True)
for rank, trend in enumerate(sorted_trends[:3], start=1):
print(f"{rank}. {trend['hashtag']} ({trend['volume']} tweets)")
1. #AI (45000 tweets)
2. #Coding (22000 tweets)
3. #Python (15000 tweets)
Without the lambda, sorting by volume would need a separate named function defined purely to extract one dictionary key, used exactly once and never again.
Recursive Functions
A recursive function is a function that calls itself, working on a smaller version of the same problem each time, until it reaches a case simple enough to answer directly.
def factorial(num):
if num == 0 or num == 1:
return 1
else:
return num * factorial(num - 1)
num = 7
print(f"Factorial of {num} is {factorial(num)}")
Factorial of 7 is 5040
Two parts make a recursive function work: a base case (num == 0 or num == 1, where the function stops calling itself and just returns an answer) and a recursive case (where it calls itself with a smaller input, moving toward that base case). Skip the base case entirely and the function calls itself forever, until Python gives up with a RecursionError.
Tracing factorial(4) shows the two-phase shape every recursive function has: it keeps calling itself downward until it hits the base case, then those calls resolve upward, each one multiplying by the number it was waiting on.
Reddit: Counting Nested Comment Threads. A comment can have replies, and those replies can have their own replies, arbitrarily deep. There's no way to know the depth in advance, which is exactly the situation recursion is built for.
reddit_thread = {
"text": "Great tutorial!",
"replies": [
{
"text": "I agree, very helpful.",
"replies": [
{"text": "Helped me pass my test.", "replies": []}
]
},
{
"text": "Could you do one on OOP next?",
"replies": []
}
]
}
def count_total_comments(comment_node):
total = 1 # count the current comment itself
for reply in comment_node["replies"]:
total += count_total_comments(reply) # recurse into each reply
return total
total_count = count_total_comments(reddit_thread)
print(f"Total comments in this thread: {total_count}")
Total comments in this thread: 4
Here the base case is implicit: a comment with an empty "replies" list simply doesn't loop at all, and returns 1 on its own.
Higher-Order Functions
A higher-order function is any function that takes another function as an argument, returns a function, or both. filter, map, and reduce are all higher-order functions themselves, since every one of them accepts a function as its first argument.
def apply_to_each(func, iterable):
return [func(x) for x in iterable]
def square(x):
return x * x
numbers = [1, 2, 3, 4, 5]
squared_numbers = apply_to_each(square, numbers)
print(squared_numbers)
[1, 4, 9, 16, 25]
The other direction, a function that returns a function, is called a closure, and it's worth slowing down on:
def create_adder(x):
def adder(y):
return x + y
return adder
add_15 = create_adder(15)
print(add_15(10))
25
create_adder(15) doesn't add anything by itself; it builds and returns a brand-new adder function that has permanently remembered x = 15. add_15 is that returned function, and every time it's called it still has access to the 15 from the call that created it, even though create_adder itself finished running long ago. This is the mechanism behind a lot of customizable, reusable code: instead of one rigid add15() function, create_adder can stamp out an add_5, an add_100, or any other adder, on demand.
AWS: Execution Timers for Billing. Cloud billing platforms need to measure how long arbitrary customer code takes to run, without knowing in advance what that code does. A higher-order "wrapper" function solves this: it accepts the customer's function as an argument, times it, and reports the result.
import time
def execution_timer(customer_function, data):
print("AWS Cloud: Starting execution timer...")
start_time = time.time()
customer_function(data) # the customer's own function, passed in as an argument
end_time = time.time()
duration = end_time - start_time
print(f"AWS Cloud: Execution finished. Billed for {duration:.5f} seconds.\n")
def process_data(data_list):
print(" Customer Code: Processing data...")
time.sleep(0.5)
print(f" Customer Code: Processed {len(data_list)} items.")
user_data = [1, 2, 3, 4, 5]
execution_timer(process_data, user_data)
execution_timer never needs to know what process_data actually does; it just needs to know it's a function it can call. Any other customer function could be passed into the exact same execution_timer without changing a single line of it.
Quick Reference Summary
| Tool | Takes | Returns | Use it when |
|---|---|---|---|
enumerate(iterable) |
one iterable | pairs of (index, value) |
you need each item's position while looping |
zip(a, b, ...) |
two or more iterables | tuples pairing up matching positions | combining parallel sequences (use lists/tuples, not sets) |
filter(func, iterable) |
a function + an iterable | only the items where func is truthy |
narrowing a list down by a condition |
map(func, iterable) |
a function + an iterable | every item transformed by func |
applying the same operation to every item |
reduce(func, iterable) |
a function + an iterable (from functools) |
a single collapsed value | rolling totals, products, or combining everything into one result |
lambda params: expr |
any number of parameters | the expression's value | a small, throwaway function, often passed straight into filter/map/sorted |
| Recursive function | itself, on a smaller input | eventually, a base-case value | problems with unknown or arbitrary depth (nested data, tree structures) |
| Higher-order function | and/or returns another function | varies | building flexible, reusable wrappers around other functions |
filter, map, and reduce all answer the same underlying question, "what do I do with every item in this collection?", just with three different endings: keep some, transform all, or combine everything into one. Lambda, recursion, and higher-order functions aren't separate tools so much as three different answers to "what is a function allowed to look like?" Once those two ideas are separate in your head, reading unfamiliar functional-style code gets a lot less intimidating.