Skip to content

Everything for WordPress, web development — and beyond

🐍 Functional programming in Python

🐍 Functional programming in Python

Any Python developer sooner or later hears: "try writing in a functional style, your code will be cleaner." But when you open the documentation, you see map, filter, and reduce with lambdas and don't understand where to start.

The problem isn't that functional programming is difficult. The problem is that most guides either dive into academic Haskell thickets or limit themselves to syntax without explaining "why." Python, being a multi-paradigm language, provides exactly as many functional tools as you need for everyday tasks, without extremes.

In this material, a practical breakdown of Python's functional capabilities: from list comprehensions to lambda functions, with real examples and scenarios where each tool genuinely simplifies code.

💡 Quick overview:

  • Breaking down list comprehensions, why they're better than loops and when to use them
  • Mastering generators: yield, memory savings and generator expressions
  • Walking through map + filter + reduce: practical examples of replacing loops
  • Learning to write lambda functions and understanding where they're appropriate and where they harm readability

Functional programming, what are we talking about

Functional programming is an approach where a program is built from mathematical functions: they take immutable input data and return a result without changing anything outside. No side effects, no modifying global state.

In pure functional languages like Haskell, "pure" and "impure" (interacting with the outside world) parts of the program are strictly separated. This approach allows formal proof of code correctness: the compiler guarantees the absence of unexpected side effects.

Python wasn't designed as a functional language. It's multi-paradigm: object-oriented, procedural, and yes, functional too. Writing in Python in a purely functional style is like hammering nails with a microscope: technically possible, but uncomfortable and unnatural. However, taking individual functional tools and embedding them in familiar code is a working and useful pattern.

List comprehensions, Python's main functional tool

The bread and butter of functional programming is working with lists. Select elements by condition, transform each element, build a new list from an existing one, all of this in Python is covered by list comprehensions.

Before, procedural loop:

1def filter_odd(li):
2 result = []
3 for i in li:
4 if i % 2 == 1:
5 result.append(i)
6 return result
7
8print(filter_odd([2, 4, 6, 7, 8, 1, 19, 200, 42, 31]))

After, list comprehension:

1li = [2, 4, 6, 7, 8, 1, 19, 200, 42, 31]
2odd_numbers = [x for x in li if x % 2 == 1]
3print(odd_numbers) # [7, 1, 19, 31]

Four lines of procedural code compressed into one. Readability didn't suffer: the syntax [expression for element in iterator if condition] is intuitively clear, "take x from li if x is odd."

And this isn't just syntactic sugar. List comprehension runs faster than an equivalent loop with .append() because it executes at the C level, not the Python interpreter level.

What is an iterator

List comprehension relies on the concept of an iterator, an object that returns the next element of a sequence when next() is requested. Any object implementing the __iter__ method is called iterable. List, string, tuple, dictionary, set, they're all iterable.

1spam_iter = iter("foobar")
2result = "".join([c.upper() for c in spam_iter])
3print(result) # FOOBAR

Here iter() returns a string iterator, and the list comprehension iterates through it, applying .upper() to each character.

Generators, lazy sequences

An iterator can be created not only from an existing collection, but also using a generator function. Instead of return, it uses yield: the function "falls asleep," remembering its state, and on the next next() call continues from the same place.

1def gen(max_val):
2 i = 1
3 while i < max_val:
4 yield i
5 i += 1
6
7g = gen(1000)
8for _ in range(12):
9 print(next(g))

This code will print numbers from 1 to 12. The gen function doesn't create a list of a thousand elements in memory, it generates values one by one, on demand. For sequences of millions of records (log file lines, database stream), the difference in memory consumption is orders of magnitude.

Generator expressions

Compact syntax: the same as list comprehensions, but with parentheses instead of square brackets.

1g = (c.upper() for c in "foobar")

This isn't a tuple, this is a generator. The rule is simple: square brackets → list (eager, all in memory), parentheses → generator (lazy, one element at a time).

Generators are a bridge between procedural and functional style in Python: they provide lazy evaluation without diving into monad theory.

map, filter and reduce: three pillars of functional processing

Three built-in functions that in functional languages are the foundation of everything. Python implements them in its own way, and it's important to know the nuances of Python-specific versions.

map, apply a function to each element

1def square(x):
2 return x * x
3
4result = map(square, [1, 2, 3])
5print(list(result)) # [1, 4, 9]

map takes a function and a sequence, and returns an iterator with the results of applying the function to each element. In Python 3, map returns an iterator, not a list, so you need list() to see the result.

For simple transformations, list comprehension often reads better:

1[x * x for x in [1, 2, 3]] # same thing, but more familiar

filter, select elements by condition

1def is_upper(c):
2 return c == c.upper()
3
4result = filter(is_upper, "FreedominObscureandoutlandishcOde")
5print(list(result)) # ['F', 'O', 'O']

filter keeps only those elements for which the predicate function returns True. Like map, it returns an iterator in Python 3.

reduce, fold a sequence into a single value

Unlike map and filter, reduce doesn't live in the built-in scope, but in the functools module. It sequentially applies a function to elements, accumulating the result.

1from functools import reduce
2
3def add(a, b):
4 return a + b
5
6print(reduce(add, range(1, 6))) # 15

Under the hood: ((((1 + 2) + 3) + 4) + 5) = 15. For summing numbers in Python there's the built-in sum(), so reduce is more often used for non-standard folds: build a tree from a flat list, merge nested dictionaries, compute the greatest common divisor of a sequence.

Lambda functions, anonymous helpers

When a function is needed exactly once and its body fits in one expression, use lambda:

1lambda arguments: expression

The same filter example can be rewritten without a separate def:

1result = list(filter(lambda c: c == c.upper(), "FreedominObscureandoutlandishcOde"))

Lambda is a compromise. Plus: no need to declare a separate function for one trivial check. Minus: readability drops if the expression becomes more complex than a couple of operations. Rule of thumb: if the lambda doesn't fit on one line or you start nesting lambdas inside each other, extract a regular def.

Where lambdas are actually useful

The most common scenario is sorting by a non-standard key:

1users = [{"name": "Alice", "age": 31}, {"name": "Bob", "age": 25}]
2users.sort(key=lambda u: u["age"])

Or quick transformation in map/filter when the body is truly trivial:

1squares = list(map(lambda x: x * x, range(10)))

But if you're reaching for a lambda for logic with four or five operations, stop. A function name works as documentation, and def provides space for a docstring.

⁉️🤔 Common questions

When is a list comprehension better than map?

A list comprehension like [x*2 for x in data] reads more naturally than list(map(lambda x: x*2, data)). A built-in operation with a lambda has two levels of indirection, while a list comprehension has one. But if the function already exists as a separate def, map with it is concise. List comprehension wins in readability for simple transformations, especially with filtering through [... if ...]. For complex logic with intermediate variables, a regular for loop is unbeatable.

Generator or list, which to choose?

If the result is needed multiple times (iteration, indexing, length), take a list. If you iterate once and the data volume is large, a generator will save memory. A list of a million integers takes about 8 MB, a generator for the same range takes less than a kilobyte. But a generator can't be "rewound" or accessed by index. Practical rule: data from a file or database, generator; result for repeated use, list().

Do lambdas harm code readability?

In small doses and for trivial logic, no. key=lambda x: x["price"] reads instantly. But as soon as a lambda stops being "one thought," extract it into a regular function. Teams with code review limit lambdas to one expression without nesting. If a colleague needs more than five seconds to understand it, rewrite it as a def. The name serves as documentation: def by_price(item): return item["price"] is clearer than an anonymous lambda.

How much slower is functional Python than procedural?

For most everyday tasks, the difference is negligible, fractions of a microsecond. List comprehension is often faster than an equivalent loop because it's implemented in C. The bottleneck in Python programs isn't the choice between map and a loop, but input/output, network requests, and architecture in general. Write clearly, profile hot spots with cProfile, and optimize only what actually slows things down.

What to use in your code today

Python's functional tools aren't dogma, but levers. They don't replace procedural and object-oriented code, but complement it at specific points.

Start with list comprehensions, this is the most common and safe application. Master generators: as soon as you see a temporary list that's used once, ask yourself, shouldn't this be replaced with yield? Use map and filter where they genuinely simplify a transformation chain. Save lambdas for key= in sorting and trivial callbacks.

Functional style in Python isn't about "purity," but about expressiveness. If after replacing a loop with a list comprehension the code became clearer, you did everything right. Try rewriting one problematic method in a functional key and compare the diff: often the result speaks for itself.