AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Functional Programming in Python

📚 Python Mastery⏱️ 24 min read🎓 Grade 8
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 24 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

Open any Python program that works with lists of things — runs scored ball by ball in an over, amounts in a list of UPI transactions, marks scored by students in a class test — and you will notice the same shape showing up again and again: go through every item one by one, do something to it, and either keep it, change it, or combine it with the rest. You write a for loop, an empty list to collect results, an if check, an accumulator variable that starts at zero. It works, but you type nearly the same six lines every single time you need to transform or filter data. Functional programming is not a new language feature — it is a different habit of writing that same logic, one where "do something to every item" and "keep only some items" and "combine everything into one answer" become named tools you call directly, instead of loops you rebuild by hand each time.

What "Functional" Actually Means

In ordinary Python, you already treat numbers and strings as values: you can store them in a variable, pass them to a function, or get one back as a return value. The core idea of functional programming is to treat functions themselves the same way — as values. A function is no longer just an instruction you run; it becomes something you can store in a variable, hand to another function as an argument, or receive as a return value. When a language lets you do all three of those things with functions, programmers say functions are "first-class" in that language. Python is one of those languages, and this single property is what makes everything else in this chapter possible.

A second habit that goes with functional style is preferring pure functions — functions whose only job is to take an input and hand back an output, without quietly changing anything else in your program. You will see exactly why this matters in a moment, with a concrete example rather than a rule to memorise.

Functions as Values You Can Pass Around

Start with something you already know how to write.

def double(x):
    return x * 2

print(double(5))   # 10

double is a normal function. Now watch what happens when you assign it to another name, without the parentheses:

def double(x):
    return x * 2

f = double        # no parentheses — we are copying the function itself
print(f(5))       # 10

Writing double without parentheses refers to the function object itself — the block of instructions — while double() would call it. Assigning f = double makes f point to the exact same function, so f(5) runs it and prints 10, identical to double(5). This is the first sign that functions behave like values in Python: you can put them in a box (a variable) just like a number.

The second sign is that you can hand a function to another function as an argument:

def double(x):
    return x * 2

def apply_twice(func, value):
    return func(func(value))

print(apply_twice(double, 3))

Trace this carefully, because it is the pattern behind everything later in the chapter. apply_twice receives two arguments: the function double (stored in the parameter named func) and the number 3 (stored in value). Inside, it computes func(func(value)). The inner call is func(3), which is really double(3), giving 6. The outer call is then func(6), which is double(6), giving 12. So apply_twice(double, 3) prints 12. Notice that apply_twice never mentions doubling anywhere in its own body — it just applies whatever function you hand it, twice. A function that takes another function as an argument (or returns one) like this is called a higher-order function. apply_twice is one; so, as you will soon see, are Python's built-in map, filter, and reduce.

The third and last sign of first-class functions is that a function can build and return another function:

def make_multiplier(n):
    def multiplier(x):
        return x * n
    return multiplier

triple = make_multiplier(3)
print(triple(7))    # 21

Here make_multiplier(3) does not compute a number — it defines a small function called multiplier that remembers the value n = 3 (this remembering is called a closure) and returns that function. triple now refers to that returned function, so triple(7) runs 7 * 3 and prints 21. You have just built a custom "multiply by 3" function out of a generic "multiply by anything" factory — something you cannot do at all in a language where functions are not values.

Pure Functions: Why Predictability Matters

Compare these two functions, both meant to add bonus runs to a player's score:

def add_bonus_runs(runs):
    return runs + 5          # pure: only depends on its input

total_runs = 0

def add_to_total(runs):      # impure: changes something outside itself
    global total_runs
    total_runs += runs
    return total_runs

add_bonus_runs is pure: call it with the same input, always get the same output, and it touches nothing outside itself. add_to_total is impure: it reads and rewrites the variable total_runs, which lives outside the function. Call add_to_total(4) and you get 4 back; call it again with the exact same argument, 4, and this time you get 8 — same input, different output, because the function depends on hidden state that changed between calls. Pure functions are easier to test (you only ever need to check input against output), easier to reason about (reading the function body tells you everything it does), and safe to run in any order or combine freely — which is exactly why the tools in the rest of this chapter, map, filter, and reduce, are all designed to work with pure functions.

Common misconception: "functional programming means you're not allowed to use loops, variables, or if statements in Python." This is false. Python is a multi-paradigm language — it does not force one style. Functional programming here means preferring pure functions and higher-order tools like map/filter/reduce for the specific job of transforming and combining data, because that style tends to produce shorter, more predictable code for that job. You will still use ordinary loops and variables everywhere else in your programs, including inside the functions you pass to map and filter when the logic gets complicated.

Anonymous Functions: lambda

Writing a full def block for a tiny, one-off function like "square a number" feels heavy:

def square(x):
    return x * x

Python lets you write the same thing as a single expression using the lambda keyword:

square = lambda x: x * x
print(square(6))    # 36

lambda x: x * x means "a function that takes one parameter, x, and returns x * x" — there is no return keyword because the expression after the colon is automatically the result. A lambda can take several parameters, separated by commas, exactly like a normal function: lambda a, b: a + b. The one hard rule is that a lambda's body must be a single expression — it cannot contain statements such as if blocks with multiple lines, loops, or print calls. A conditional value is still allowed, because Python's ternary form a if condition else b is an expression, not a statement: lambda x: "boundary" if x >= 4 else "no boundary" is perfectly legal. lambda is almost never assigned to a name like square above in real code — its real use is being written inline, right at the spot where a function is needed, which is exactly what map and filter want.

Transforming Every Element: map()

Suppose an over of cricket produces these runs, ball by ball:

runs = [4, 0, 6, 1, 2, 6]

Imagine a hypothetical "power over" rule where every run scored counts double. Written as a loop, you would do this:

doubled = []
for r in runs:
    doubled.append(r * 2)

map() replaces that entire pattern with one line: it applies a function to every element of an iterable and hands back the transformed sequence, one element per element.

doubled = list(map(lambda r: r * 2, runs))
print(doubled)     # [8, 0, 12, 2, 4, 12]

Trace it: map takes each value from runs4, 0, 6, 1, 2, 6 — and passes it through lambda r: r * 2, producing 8, 0, 12, 2, 4, 12 in the same order. Two details matter here. First, map in Python 3 does not give you a list directly — it gives you a special lazy object called a map object, which only produces values as they are requested. If you write print(map(lambda r: r * 2, runs)) without wrapping it in list(...), you will see something like <map object at 0x7f8a1c0d5f40> instead of the numbers you expect — this is a very common beginner mistake. Wrapping it in list() forces Python to actually walk through and collect every value. Second, map never modifies runs itself; runs still holds [4, 0, 6, 1, 2, 6] after this code runs, because map builds a brand-new sequence rather than changing the original — a direct consequence of using a pure function inside it.

Selecting What You Need: filter()

Now suppose you want only the balls that were boundaries — runs of 4 or more. As a loop:

boundaries = []
for r in runs:
    if r >= 4:
        boundaries.append(r)

filter() replaces this: it keeps only the elements for which a given function returns True, and drops the rest.

boundaries = list(filter(lambda r: r >= 4, runs))
print(boundaries)    # [4, 6, 6]

Trace it against runs = [4, 0, 6, 1, 2, 6]: the test r >= 4 is checked for every ball — 4 >= 4 is True (kept), 0 >= 4 is False (dropped), 6 >= 4 is True (kept), 1 >= 4 is False (dropped), 2 >= 4 is False (dropped), 6 >= 4 is True (kept). What survives, in original order, is [4, 6, 6]. Like map, plain filter() also returns a lazy filter object rather than a list, so it needs the same list(...) wrapping if you want to see or store the results directly. The function you pass to filter must always return something Python treats as True or False — this kind of function is sometimes called a predicate.

Combining Everything Into One Answer: functools.reduce()

map transforms element-for-element and filter keeps a subset, but neither can turn a whole list into a single combined answer, such as a total. That is the job of reduce, which lives in Python's functools module rather than being a plain built-in like the previous two:

from functools import reduce

total = reduce(lambda acc, r: acc + r, runs, 0)
print(total)    # 19

reduce(function, iterable, start) keeps a running value, usually called an accumulator, that starts at start. For every element in the iterable, it replaces the accumulator with function(accumulator, element). Trace it step by step against runs = [4, 0, 6, 1, 2, 6] starting from 0:

  • acc = 0, next element 4 → 0 + 4 = 4
  • acc = 4, next element 0 → 4 + 0 = 4
  • acc = 4, next element 6 → 4 + 6 = 10
  • acc = 10, next element 1 → 10 + 1 = 11
  • acc = 11, next element 2 → 11 + 2 = 13
  • acc = 13, next element 6 → 13 + 6 = 19

After the last element, reduce returns the final accumulator, 19, which matches 4 + 0 + 6 + 1 + 2 + 6. The starting value 0 matters: it is the accumulator's value before any element has been processed, and choosing it wrong changes the answer — starting from 10 instead of 0 here would (wrongly) produce 29. In fact, "sum everything up" is such a common use of reduce that Python gives you a dedicated built-in for exactly this case: sum(runs) gives the same 19 without importing anything. You will still need real reduce for combinations that are not plain addition — finding the highest score, building a running product, or joining strings together — because there is no single built-in shortcut for every possible way of combining values.

Chaining the Three Together

The real power shows up when you connect filter and reduce directly, without stopping to build a list in between. Suppose you want the total runs scored only off boundary balls:

from functools import reduce

boundary_total = reduce(
    lambda acc, r: acc + r,
    filter(lambda r: r >= 4, runs),
    0
)
print(boundary_total)    # 16

reduce does not require its second argument to already be a list — any iterable will do, including the lazy object that filter produces. So Python first filters runs down to boundary balls, 4, 6, 6, one at a time, and reduce immediately consumes each one as it appears: 0 + 4 = 4, then 4 + 6 = 10, then 10 + 6 = 16. The final answer, 16, is the combined runs from boundaries only, and Python never had to build the intermediate list [4, 6, 6] in memory at all — it processed one value at a time straight through the pipeline. The diagram below shows this exact pipeline visually.

Chaining filter() and reduce() runs = [4,0,6,1,2,6] filter(r >= 4) keep 4? yes keep 0,1,2? no keep 6,6? yes [4, 6, 6] boundaries reduce 0+4=4 4+6=10 10+6=16 16 final total

The Pythonic Alternative: List Comprehensions

Python offers its own built-in shorthand for the exact jobs map and filter do, called a list comprehension:

doubled_lc   = [r * 2 for r in runs]        # same as list(map(...))
boundaries_lc = [r for r in runs if r >= 4]  # same as list(filter(...))

print(doubled_lc)     # [8, 0, 12, 2, 4, 12]
print(boundaries_lc)  # [4, 6, 6]

Both comprehensions produce identical results to the map/filter versions above — you can verify this by comparing them line by line against the traces already worked out. Most experienced Python programmers reach for a comprehension over map or filter when the transformation is simple, because [r * 2 for r in runs] reads left to right almost like English, while list(map(lambda r: r * 2, runs)) needs an extra list() wrapper and a lambda. However, map and filter are still worth knowing well for two reasons: they are the same vocabulary used for this pattern in many other languages (JavaScript's .map() and .filter() array methods work almost identically), and unlike a comprehension, they can accept an already-existing named function directly — list(map(double, runs)) — with no lambda needed at all. There is no comprehension form for reduce; combining a whole sequence into one value still needs functools.reduce, a plain loop, or a specialised built-in like sum(), max(), or min().

A Full Worked Example: Class Test Marks

Here is a problem that uses all three tools together, with a slightly higher difficulty than the cricket example. A class test out of 100 marks has a passing mark of 33. Given a list of marks, find the average mark among students who passed:

from functools import reduce

marks = [45, 28, 67, 33, 19, 88]

passing = list(filter(lambda m: m >= 33, marks))
print(passing)    # [45, 67, 33, 88]

total = reduce(lambda acc, m: acc + m, passing, 0)
average = total / len(passing)
print(average)    # 58.25

Trace the filter step first: checking each mark against m >= 3345 passes, 28 fails, 67 passes, 33 passes (33 itself meets the cutoff, since the test is >= not >), 19 fails, 88 passes. The surviving marks, in order, are [45, 67, 33, 88]. Then reduce sums them: 0 + 45 = 45, 45 + 67 = 112, 112 + 33 = 145, 145 + 88 = 233, giving total = 233. Finally, average = 233 / 4 = 58.25, since four students passed. Now add one more step: give each passing student one bonus point for every 10 marks scored, purely as an illustrative rule (not an official grading scale) to see map working on the filtered results:

bonus_points = list(map(lambda m: m // 10, passing))
print(bonus_points)    # [4, 6, 3, 8]

Using integer division //, which discards the remainder: 45 // 10 = 4, 67 // 10 = 6, 33 // 10 = 3, 88 // 10 = 8, matching [4, 6, 3, 8] exactly. This single problem — filter the passing students, then either reduce them to an average or map them to bonus points — is the same three-step shape you will meet constantly once you start noticing it: keep what matters, transform what remains, combine into one answer.

Trace the Code: Check Your Understanding

  1. What does this print?

    prices = [120, 45, 300, 15, 80]
    cheap = list(filter(lambda p: p < 100, prices))
    print(cheap)
    

    Answer: [45, 15, 80] — every price checked against p < 100: 120 fails, 45 passes, 300 fails, 15 passes, 80 passes, kept in original order.

  2. What does this print, and why is it not a list of numbers?

    values = [1, 2, 3]
    result = map(lambda x: x + 1, values)
    print(result)
    

    Answer: something like <map object at 0x...>, not [2, 3, 4], because map returns a lazy map object in Python 3 rather than a list — printing it directly shows the object, not its contents. Wrapping it as list(result) would show [2, 3, 4].

  3. Trace this reduce call step by step and give the final value:

    from functools import reduce
    nums = [2, 3, 4]
    product = reduce(lambda acc, n: acc * n, nums, 1)
    

    Answer: acc starts at 1. Step 1: 1 * 2 = 2. Step 2: 2 * 3 = 6. Step 3: 6 * 4 = 24. Final value: 24. (Note the starting value here is 1, not 0, because 0 would multiply everything to zero — the correct starting value depends on the combining operation.)

  4. Is this function pure or impure, and what would go wrong if you relied on it inside a map call?

    log = []
    def record(x):
        log.append(x)
        return x * 2
    

    Answer: impure — it modifies the external list log every time it runs, in addition to returning a value. Using it inside map would still produce correct transformed numbers, but it would also silently grow log as a side effect, and if map's laziness means the function only runs when you actually consume the result (for example, once inside list(...)), the timing of when items get added to log becomes easy to get wrong — exactly the kind of hidden, timing-dependent behaviour pure functions are designed to avoid.

Summary

Functional programming in Python is not a separate language — it is a way of using a property Python already has: functions are values, so they can be stored in variables, passed as arguments, and returned from other functions, making higher-order functions like map, filter, and apply_twice possible. A pure function depends only on its input and changes nothing outside itself, which is what makes it safe and predictable to hand to tools like map and filter. lambda gives you a compact, inline way to write small one-expression functions without a full def block. map(function, iterable) transforms every element and returns a lazy map object — remember to wrap it in list(...) to see the results. filter(function, iterable) keeps only the elements for which the function returns True, using the same lazy-object rule. functools.reduce(function, iterable, start) folds an entire sequence down into a single accumulated value, one element at a time, starting from start. All three can be chained directly, since reduce and list() can consume any iterable, including the lazy object another function produced. For simple transformations and filters, Python's own list comprehensions are usually the more readable choice; map, filter, and especially reduce remain essential for named functions, more complex pipelines, and for the vocabulary they share with functional-style code in other languages.

Practice Exercises

Now it is time to practice! Complete these challenges to solidify your understanding:

  • Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
  • Exercise 2: Find a real-world example where functional programming in python is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
  • Exercise 3: Create a mind-map connecting functional programming in python to at least 3 other topics you have studied.
← Regular Expressions: Pattern Matching PowerObject-Oriented Programming: Classes and Objects →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn