Functional Programming in Python — Beginners

This notebook is designed as an accessible introduction to functional programming concepts using core Python features. You will learn how to use map, filter, and reduce to process and analyze simple, realistic datasets. All examples use straightforward Python data structures (like lists and dictionaries) relevant to everyday data problems.

The notebook emphasizes “pure functions” (functions with no side effects that always return the same output for the same input) and demonstrates functional programming techniques on small datasets representing orders and users. As you work through the exercises, you’ll see how to: - Write and use pure functions in Python. - Work with built-in higher-order functions such as map, filter, and reduce. - Apply these concepts on practical real-world-like datasets to solve problems clearly and concisely.

Whether you’re new to functional programming or looking for practical Python applications, this notebook offers concise explanations and hands-on practice for beginners.

Dataset

Code
# ## Example Orders Dataset
# This code defines a sample list of order dictionaries, each representing 
# an order with an ID, customer name, amount, and status.
def get_example_orders():
    """
    Returns a list of example order dictionaries.
    Each order contains:
        - id: Unique order identifier
        - customer: Customer name
        - amount: Order amount (float)
        - status: Order status (string)
    """
    return [
        {"id": 1, "customer": "Alice", "amount": 120.0, "status": "paid"},
        {"id": 2, "customer": "Bob", "amount": 0.0, "status": "cancelled"},
        {"id": 3, "customer": "Carol", "amount": 75.5, "status": "paid"},
        {"id": 4, "customer": "Dan", "amount": -10.0, "status": "refund"},
        {"id": 5, "customer": "Eve", "amount": 310.0, "status": "paid"},
    ]

orders = get_example_orders()
orders

Pure Function

A pure function is a function that meets two important criteria:

  1. The output depends only on the input arguments.
    • Given the same input value(s), a pure function will always return the same result. It does not rely on any external state, global variables, or data that can change during program execution.
  2. No side effects.
    • The function does not modify any external state, variables, files, databases, user input/output devices, or data outside its own scope.
    • Calling a pure function only produces an output value; it does not cause any observable effect elsewhere in the system (such as printing, writing a file, or changing a global variable).

Why are pure functions important?

  • Predictability: Since pure functions always return the same output for the same input, they are easy to test and reason about.
  • Debuggability: Fewer unexpected interactions, since no outside state is changed.
  • Composability: Pure functions are easy to combine with other functions to build more complex logic without creating unexpected bugs.
  • Enables Functional Programming: Core concept in functional programming, which relies on maintaining immutability and avoiding side effects.

Example of a Pure Function (in Python):

def add(x, y):
    return x + y
  • This function is pure: it always returns the sum of x and y, and does not affect any data outside its own scope.

Example of an Impure Function:

total = 0
def add_to_total(x):
    global total
    total += x
    return total
  • This function is impure: its output depends on and changes the external variable total.
Code
# This function applies a discount to a given amount at a specified rate.
# 'amount' is the original price, and 'rate' is the discount rate (e.g., 0.1 for 10% off).
# The function calculates the discounted price and rounds it to 2 decimal places.

def apply_discount(amount, rate):
    return round(amount * (1 - rate), 2)

# Example usage: apply a 10% discount to $100
apply_discount(100, 0.1)

map() – Apply a Function to Every Element of an Iterable

The map() function is a fundamental tool in functional programming. It takes two arguments: a function and an iterable (such as a list, tuple, etc.). map() applies the given function to each item in the iterable, producing an iterator (known as a map object) of the results.

Syntax: map(function, iterable)

  • function: A function to apply to each element. This can be a built-in function, a lambda (anonymous) function, or any user-defined function that accepts a single argument.
  • iterable: The collection of elements to apply the function to (for example, a list of numbers).

To retrieve the results (which are lazily generated), you usually convert the map object into a list or another collection type: result = list(map(function, iterable))

Advantages: - Eliminates the need for explicit loops when transforming items. - Offers concise, readable code—especially when working with large datasets. - Useful for applying the same operation to every element, such as squaring numbers, converting strings to uppercase, etc.

Example: Square every item in a list of numbers numbers = [1, 2, 3, 4, 5] squared = list(map(lambda x: x**2, numbers)) # Output: [1, 4, 9, 16, 25]

Code
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
print(squared)  # [1, 4, 9, 16, 25]

# With named function
def square(x):
    return x**2

squared = list(map(square, numbers))
print(squared)  # [1, 4, 9, 16, 25]

# Multiple iterables
list1 = [1, 2, 3]
list2 = [10, 20, 30]
summed = list(map(lambda x, y: x + y, list1, list2))
print(summed)  # [11, 22, 33]
Code
# Using map() with orders dataset
order_summaries = list(map(
    lambda o: {
        "id": o["id"],
        "customer_upper": o["customer"].upper(),
        "amount": o["amount"],
        "is_high_value": o["amount"] >= 100,
        "status": o["status"],
    },
    orders,
))

print("\nUsing map() with orders:")
for row in order_summaries:
    print(row)

filter() - Select elements based on a condition

filter() is a built-in Python function used to extract elements from an iterable (like a list, tuple, etc.) where a specified function returns True for those elements.

The syntax is: filter(function, iterable) - function should take a single argument and return True or False. - Only items for which the function returns True are included in the resulting filter object (which can be converted to a list, tuple, etc.).

This is especially useful when you want to “filter out” elements that don’t meet certain criteria, such as removing negative numbers, keeping only even numbers, or selecting valid records from a dataset.

Example: Select even numbers from a list numbers = [1, 2, 3, 4, 5, 6] evens = list(filter(lambda x: x % 2 == 0, numbers)) # Output: [2, 4, 6]

In practice, filter() can make your code more concise and readable by clearly expressing “selection” logic without manual loops.

Code
# Filter even numbers
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)  # [2, 4, 6, 8, 10]

# Filter strings by length
words = ["apple", "cat", "elephant", "dog", "butterfly"]
long_words = list(filter(lambda word: len(word) > 4, words))
print(long_words)  # ['apple', 'elephant', 'butterfly']

# Using a named function
def is_prime(n):
    if n < 2:
        return False
    for i in range(2, int(n**0.5) + 1):
        if n % i == 0:
            return False
    return True

primes = list(filter(is_prime, range(20)))
print(primes) 
Code
# Filter orders to get only those with status "paid"
paid_orders = list(filter(lambda order: order["status"] == "paid", orders))
print("Paid orders:", paid_orders)

# Filter orders with positive amounts
positive_amount_orders = list(filter(lambda order: order["amount"] > 0, orders))
print("Orders with positive amounts:", positive_amount_orders)

# Filter orders that were cancelled
cancelled_orders = list(filter(lambda order: order["status"] == "cancelled", orders))
print("Cancelled orders:", cancelled_orders)

reduce() - Aggregate values

reduce() is used when you want to combine many values into one final result (for example: total revenue, product of numbers, or a merged dictionary).

It applies a function cumulatively from left to right: - The function receives two values each step: an accumulator (acc) and the next item (x). - It returns a new accumulator value. - After all items are processed, that final accumulator is the result.

General form: reduce(function, iterable, initial_value)

  • function: usually lambda acc, x: ...
  • iterable: list/tuple/any iterable to process
  • initial_value (recommended): starting value for the accumulator (for sums often 0, for products often 1)

Why use an initial value? - It makes behavior predictable. - It prevents errors on empty iterables. - It makes your intent clearer to readers.

Example idea with orders: 1. Filter to valid paid orders. 2. Map to amounts. 3. Reduce amounts to one number (total_revenue).

Code
from functools import reduce

# Sum all numbers
numbers = [1, 2, 3, 4, 5]
total = reduce(lambda x, y: x + y, numbers)
print(total)  # 15

# Find maximum
max_val = reduce(lambda x, y: x if x > y else y, numbers)
print(max_val)  # 5

# Product of all numbers
product = reduce(lambda x, y: x * y, numbers)
print(product)  # 120

# Using operator for cleaner code
import operator
total = reduce(operator.add, numbers)
product = reduce(operator.mul, numbers)
print(f"Sum: {total}, Product: {product}")  # Sum: 15, Product: 120

List Comprehensions - Pythonic alternatives

List comprehensions provide a concise and readable way to create new lists by applying an expression to each item in an iterable (like a list or range), optionally including an if-condition to filter items.

Syntax: [expression for item in iterable if condition]

  • The expression is evaluated for each item in the iterable.
  • The optional if condition allows you to filter which items are included.

While not strictly part of functional programming, list comprehensions are considered more “Pythonic” and are usually more readable and efficient than using map() and filter() separately. For example, instead of writing list(map(lambda x: x*x, numbers)) or list(filter(lambda x: x % 2 == 0, numbers)), a list comprehension can be used to achieve the same results more simply and clearly.

List comprehensions can also be nested, which is useful for flattening lists of lists, or constructing complex lists in a single line of code.

Code
numbers = [1, 2, 3, 4, 5]

# Equivalent to map()
squared = [x**2 for x in numbers]
print(squared)  # [1, 4, 9, 16, 25]

# Equivalent to filter()
evens = [x for x in numbers if x % 2 == 0]
print(evens)  # [2, 4]

# Both map and filter
even_squares = [x**2 for x in numbers if x % 2 == 0]
print(even_squares)  # [4, 16]

# Nested comprehensions
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened = [item for row in matrix for item in row]
print(flattened)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]