Functional Programming

Advanced · 20 min read · ▶ live playground · ✦ checkpoint

Some code is easier when you stop asking, "Which variable should I change next?" and ask, "What transformation should happen next?" Functional programming is a style that builds programs from small functions, clear data transformations, and as little changing state as practical.

Prefer pure transformations

A pure function is a function that returns the same result for the same arguments and does not change anything outside itself. A side effect is an outside change a function makes, such as editing a list, writing a file, or changing a global name.

Use the function-as-recipe-card analogy again: a pure function is a recipe card that uses the ingredients you hand it, returns a dish, and does not rearrange the pantry.

Immutability means a value cannot be changed after it is created. Tuples and strings are immutable, so they fit this style well. You can still make a new value from an old one.

def award_bonus(record, bonus):
    player, score = record
    return (player, score + bonus)
 
 
scores = (("Mina", 80), ("Ravi", 67), ("Noor", 91))
boosted = tuple(award_bonus(record, 5) for record in scores)
 
print(scores)
print(boosted)

(('Mina', 80), ('Ravi', 67), ('Noor', 91))
(('Mina', 85), ('Ravi', 72), ('Noor', 96))

The original scores tuple stays untouched. boosted is a new tuple. That makes the data flow easier to trust: values move through transformations instead of being edited from several places.

Use map, filter, and reduce

map() applies one function to every item in an iterable. filter() keeps only the items that pass a yes-or-no function. reduce() combines many items into one final value.

These tools form a small pipeline, a series of steps where each step passes its result to the next one. Picture a row of workstations: one station adjusts scores, the next keeps passing scores, the last totals them.

from functools import reduce
 
 
scores = [62, 88, 71, 95]
 
 
def add_five(score):
    return score + 5
 
 
def is_passing(score):
    return score >= 70
 
 
def add_total(running_total, score):
    return running_total + score
 
 
curved = list(map(add_five, scores))
passing = list(filter(is_passing, curved))
total = reduce(add_total, passing, 0)
 
print(curved)
print(passing)
print(total)
 
passing_again = [score for score in curved if is_passing(score)]
print(passing_again)

[67, 93, 76, 100]
[93, 76, 100]
269
[93, 76, 100]

map and filter are lazy iterators, so list(...) makes their results visible. reduce comes from functools, and the 0 is the starting total.

The list comprehension at the end is not "less advanced." It is often more Pythonic when the transformation is short. Use map and filter when a named function makes the pipeline clearer, or when you are already working with lazy iterator tools.

Try it - reshape a score list

Run this, then change the bonus from 5 to 10. Try replacing the final reduce call with sum(score for player, score in ready).

python — playgroundlive
⌘/Ctrl + Enter to run

Pre-fill and remember with functools

functools is a standard-library module for tools that work with functions. partial() creates a new callable with some arguments already filled in.

lru_cache() is a decorator that remembers return values for recent argument combinations. LRU means "least recently used": when the cache is full, the oldest unused entry is the first one dropped.

from functools import lru_cache, partial
 
 
def label_score(prefix, player, score):
    return f"{prefix}: {player} -> {score}"
 
 
coach_label = partial(label_score, "Coach note")
 
print(coach_label("Mina", 92))
 
 
@lru_cache(maxsize=3)
def badge_for_level(level):
    print(f"building badge for level {level}")
    return f"Level {level} badge"
 
 
print(badge_for_level(2))
print(badge_for_level(2))
print(badge_for_level(3))
print(badge_for_level.cache_info())

Coach note: Mina -> 92
building badge for level 2
Level 2 badge
Level 2 badge
building badge for level 3
Level 3 badge
CacheInfo(hits=1, misses=2, maxsize=3, currsize=2)

partial is useful when a general function becomes clearer with one setting locked in. lru_cache is useful when a function is pure and expensive enough that repeating the same work would be wasteful. It is a poor fit for functions that depend on changing outside state.

cache_info() reports cache hits, cache misses, the maximum size, and the current stored entries. A cache hit means Python reused a remembered result instead of running the function body again.

Use itertools for streams

itertools is a standard-library module full of iterator-building tools. You saw lazy streams in iterators and generators; itertools gives you ready-made pieces for common stream shapes.

chain() walks through several iterables as one stream. cycle() repeats an iterable forever, so you must bound it with zip, range, or islice(), which takes a limited slice from an iterator. combinations() produces unordered pairs or groups. groupby() groups consecutive matching items.

from itertools import chain, combinations, cycle, groupby, islice
 
 
warmup = ["stretch", "breathe"]
main = ["solve puzzle", "review notes"]
 
print(list(chain(warmup, main)))
print(list(islice(cycle(["focus", "rest"]), 5)))
print(list(combinations(["Mina", "Ravi", "Noor"], 2)))
 
entries = [
    ("focused", "Mina"),
    ("focused", "Noor"),
    ("tired", "Ravi"),
    ("tired", "Ada"),
]
 
for mood, group in groupby(entries, key=lambda entry: entry[0]):
    names = [entry[1] for entry in group]
    print(mood, names)

['stretch', 'breathe', 'solve puzzle', 'review notes']
['focus', 'rest', 'focus', 'rest', 'focus']
[('Mina', 'Ravi'), ('Mina', 'Noor'), ('Ravi', 'Noor')]
focused ['Mina', 'Noor']
tired ['Ravi', 'Ada']

The detail to remember is groupby: it groups neighboring matches. If matching items are scattered, sort or arrange them first, or you will get separate groups with the same key.

Functional style is not a contest to remove every loop. Reach for it when small pure functions, lazy streams, or reusable function tools make the data flow easier to read. Keep the comprehension or loop when it says the idea more plainly.

Checkpoint

Answer all three to mark this lesson complete

Functional tools give you compact ways to express data transformations without hiding the work. Next, Pythonic power tools sharpen that compactness further, including one copy-related trap that catches even careful beginners.

+50 XP on completion