How to do functional programming in Python

The popular programming language Python is known for object-oriented programming (OOP). But Python is also well suited to functional programming. Learn what functions are available and how to use them.

What makes functional programming stand out?

The term ‘functional programming’ refers to a programming style that uses functions as the fundamental unit of code. There is a gradual gradation from purely functional languages such as Haskell or Lisp to multi-paradigm languages such as Python. The distinction between whether a language supports functional programming is not clear cut.

For functional programming to be possible in a language, it must treat functions as ‘first-class citizens’. This is the case with Python, because functions are objects, just like strings, numbers and lists. Functions can be passed as parameters to other functions or returned as return values from functions.

Functional programming is declarative

In declarative programming, you describe a problem and let the language environment find the solution. In contrast, with the imperative approach, you are responsible for giving the program step-by-step instructions on how to reach the solution. Functional programming takes the declarative approach. Python is a multi-paradigm language and can be used for both approaches.

Let’s take a look at an example of functional programming in Python. Below is a list of numbers. We want to compute the square numbers of the numbers in this list. Here’s the imperative approach:

# Calculate squares from list of numbers
def squared(nums):
    # Start with empty list
    squares = []
    # Process each number individually
    for num in nums:
        squares.append(num ** 2)
    return squares
python

Python supports a declarative approach with list comprehensions, which can be combined with functional techniques. We’ve generated the list of square numbers without using an explicit loop. The resulting code is much leaner and doesn’t require indentations:

# Numbers 0–9
nums = range(10)
# Calculate squares using list expression
squares = [num ** 2 for num in nums]
# Show that both methods give the same result
assert squares == squared(nums)
python

Pure functions are preferred over procedures

A pure function is comparable to a basic mathematical function. The term denotes a function with the following properties:

• The function returns the same result for the same arguments.

• The function only has access to its arguments.

• The function does not trigger any side effects.

These properties mean that the surrounding system does not change when a pure function is called. Consider the square function f(x) = x * x. This can easily be implemented as a pure function in Python:

def f(x):
    return x * x
# let's test
assert f(9) == 81
python

Procedures, which are common in older languages such as Pascal and Basic, stand in contrast to pure functions. Just like a function, a procedure is a named block of code that can be called multiple times. However, a procedure does not deliver a return value. Instead, procedures access non-local variables directly and modify them as needed.

In C and Java, procedures are implemented as functions with void as the return type. In Python, a function always returns a value. If there is no return statement, the special value ‘None’ is returned. So, when we talk about procedures in Python, we are referring to a function without a return statement.

Let’s look at a few examples of pure and impure functions in Python. The following function is impure because it returns a different result each time it is called:

# Function without arguments
def get_date():
    from datetime import datetime
    return datetime.now()
python

The following procedure is impure because it accesses data defined outside of the function:

# Function using non-local value
name = 'John'
def greetings_from_outside():
    return(f"Greetings from {name}")
python

The following function is impure because it modifies a mutable argument when called, which affects the surrounding system:

# Function modifying argument
def greetings_from(person):
    print(f"Greetings from {person['name']}")
    # Changing `person` defined somewhere else
    person['greeted'] = True
    return person
# Let's test
person = {'name': "John"}
# Prints `John`
greetings_from(person)
# Data was changed from inside function
assert person['greeted']
python

The following function is pure, because it returns the same result for the same argument and does not have any secondary effects:

# Pure function
def squared(num):
    return num * num
python

Recursion is used as an alternative to iteration

In functional programming, recursion is the counterpart to iteration. A recursive function calls itself repeatedly to produce a result. For this to work without the function causing an infinite loop, two conditions must be met:

  1. The recursion must terminate when a base case is reached.
  2. While recursively traversing the function, the problem must become simpler or smaller.

Python supports recursive functions. We’ll use the calculation of the Fibonacci sequence. This is representative of what’s known as the naive approach. It does not perform well for large values of n, but can be optimised by caching:

def fib(n):
    if n == 0 or n == 1:
        return n
    else:
        return fib(n - 2) + fib(n - 1)
python

How well is Python suited for functional programming?

Python is a multi-paradigm language, which means that different programming paradigms can be followed when writing programs. In addition to functional programming, object-oriented programming in Python is easy to implement.

Python has a wide range of tools for functional programming. However, in contrast to purely functional languages like Haskell, its scope is rather limited. The extent to which functional programming can be incorporated into a Python program largely depends on the person programming it. Below, you’ll find an overview of the most important functional features in Python.

Functions are first-class citizens in Python

In Python ‘everything is an object’, and this is also true for functions. Functions can be used anywhere within the language where objects are allowed. Let’s look at a concrete example. Let’s say we want to program a calculator that supports simple mathematical operations. We’ll first show the imperative approach. This uses the classic tools in structured programming such as conditional branching and assignment statements:

def calculate(a, b, op='+'):
    if op == '+':
        result = a + b
    elif op == '-':
        result = a - b
    elif op == '*':
        result = a * b
    elif op == '/':
        result = a / b
    return result
python

Now let’s consider a declarative approach to solving the same problem. Instead of using if-branching, we can map the operations with Python dict. In this case, the operation symbols serve as keys that reference corresponding function objects imported from the operator module. The resulting code is more concise and does not require branching:

def calculate(a, b, op='+'):
    # Import operator functions
    import operator
    # Map operation symbols to functions
    operations = {
        '+': operator.add,
        '-': operator.sub,
        '*': operator.mul,
        '/': operator.truediv,
    }
    # Choose operation to carry out
    operation = operations[op]
    # Run operation and return results
    return operation(a, b)
python

Next, let’s test our declarative calculate function. The assert statements show that our code works:

# Let's test
a, b = 42, 51
assert calculate(a, b, '+') == a + b
assert calculate(a, b, '-') == a - b
assert calculate(a, b, '*') == a* b
assert calculate(a, b, '/') == a / b
python

Lambdas are anonymous functions in Python

In addition to defining functions in Python via the def keyword, the language also has ‘lambdas’. These are short, anonymous (i.e. unnamed) functions that define an expression with parameters. Lambdas can be used everywhere where a function is expected or assigned to a name:

squared = lambda x: x * x
assert squared(9) == 81
python

Using lambdas, we can improve our calculate function. Instead of hard coding the available operations within the function, we pass a dict with lambda functions as values. This allows us to easily add new operations later:

def calculate(a, b, op, ops={}):
    # Get operation from dict and define noop for non-existing key
    operation = ops.get(op, lambda a, b: None)
    return operation(a, b)
# Define operations
operations = {
    '+': lambda a, b: a + b,
    '-': lambda a, b: a - b,
}
# Let's test
a, b, = 42, 51
assert calculate(a, b, '+', operations) == a + b
assert calculate(a, b, '-', operations) == a - b
# Non-existing key handled gracefully
assert calculate(a, b, '**', operations) == None
# Add a new operation
operations['**'] = lambda a, b: a** b
assert calculate(a, b, '**', operations) == a** b
python

Higher-order functions in Python

Lambdas are often used in connection with higher-order functions like map() and filter(). This allows the elements of an iterable to be transformed without using loops. The map() function takes a function and an iterable as parameters and executes the function for each element of the iterable. Let’s consider the problem of generating square numbers again:

nums = [3, 5, 7]
squares = map(lambda x: x ** 2, nums)
assert list(squares) == [9, 25, 49]
python
Note

Higher-order functions are functions that take functions as parameters or return a function.

The filter() function can be used to filter the elements of an iterable. We can extend our example so that only even square numbers are generated:

nums = [1, 2, 3, 4]
squares = list(map(lambda num: num ** 2, nums))
even_squares = filter(lambda square: square % 2 == 0, squares)
assert list(even_squares) == [4, 16]
python

Iterables, comprehensions and generators

Iterables are a core concept of Python. An iterable is an abstraction over collections whose elements can be output individually. These include strings, tuples, lists and dicts, all of which follow the same rules. The scope of an iterable can be queried with the len() function:

name = 'Walter White'
assert len(name) == 12
people = ['Jim', 'Jack', 'John']
assert len(people) == 3
python

Building upon iterables, comprehensions are used. These are well suited for functional programming and have largely replaced the use of lambdas with map() and filter().

# List comprehension to create first ten squares
squares = [num ** 2 for num in range(10)]
python

Lazy evaluation, which is common in purely functional languages, can be implemented in Python with generators. This means that data is only generated when it is accessed. This can save a lot of memory. Below is an example with a generator expression that calculates each square number on access:

# Generator expression to create first ten squares
squares = (num ** 2 for num in range(10))
python

The yield statement can be used to implement lazy functions in Python. Now, we’ll write a function that returns the positive numbers up to a given limit:

def N(limit):
    n = 1
    while n <= limit:
        yield n
        n += 1
python

What alternatives are there to Python for functional programming?

Functional programming has been popular for a while and has established itself as a major counterforce to object-oriented programming. The combination of immutable data structures with pure functions leads to code that can be easily parallelised. Functional programming is particularly useful for the transformation of data into data pipelines.

Purely functional languages with strong type systems such as Haskell or the Lisp dialect Clojure are solid options. JavaScript is also considered a functional language at its core. With TypeScript, a modern alternative with strong typing has been made available.

Tip

Want to work online with Python? You can rent webspace for your project from IONOS.

Was this article helpful?
Page top