The popular pro­gram­ming language Python is known for object-oriented pro­gram­ming (OOP). But Python is also well suited to func­tion­al pro­gram­ming. Learn what functions are available and how to use them.

What makes func­tion­al pro­gram­ming stand out?

The term ‘func­tion­al pro­gram­ming’ refers to a pro­gram­ming style that uses functions as the fun­da­ment­al unit of code. There is a gradual gradation from purely func­tion­al languages such as Haskell or Lisp to multi-paradigm languages such as Python. The dis­tinc­tion between whether a language supports func­tion­al pro­gram­ming is not clear cut.

For func­tion­al pro­gram­ming 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 para­met­ers to other functions or returned as return values from functions.

Func­tion­al pro­gram­ming is de­clar­at­ive

In de­clar­at­ive pro­gram­ming, you describe a problem and let the language en­vir­on­ment find the solution. In contrast, with the im­per­at­ive approach, you are re­spons­ible for giving the program step-by-step in­struc­tions on how to reach the solution. Func­tion­al pro­gram­ming takes the de­clar­at­ive approach. Python is a multi-paradigm language and can be used for both ap­proaches.

Let’s take a look at an example of func­tion­al pro­gram­ming in Python. Below is a list of numbers. We want to compute the square numbers of the numbers in this list. Here’s the im­per­at­ive 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 de­clar­at­ive approach with list com­pre­hen­sions, which can be combined with func­tion­al tech­niques. We’ve generated the list of square numbers without using an explicit loop. The resulting code is much leaner and doesn’t require in­dent­a­tions:

# 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 pro­ced­ures

A pure function is com­par­able to a basic math­em­at­ic­al function. The term denotes a function with the following prop­er­ties:

• 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 prop­er­ties mean that the sur­round­ing system does not change when a pure function is called. Consider the square function f(x) = x * x. This can easily be im­ple­men­ted as a pure function in Python:

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

Pro­ced­ures, 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, pro­ced­ures access non-local variables directly and modify them as needed.

In C and Java, pro­ced­ures are im­ple­men­ted 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 pro­ced­ures 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 sur­round­ing 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 al­tern­at­ive to iteration

In func­tion­al pro­gram­ming, recursion is the coun­ter­part to iteration. A recursive function calls itself re­peatedly to produce a result. For this to work without the function causing an infinite loop, two con­di­tions must be met:

  1. The recursion must terminate when a base case is reached.
  2. While re­curs­ively tra­vers­ing the function, the problem must become simpler or smaller.

Python supports recursive functions. We’ll use the cal­cu­la­tion of the Fibonacci sequence. This is rep­res­ent­at­ive 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 func­tion­al pro­gram­ming?

Python is a multi-paradigm language, which means that different pro­gram­ming paradigms can be followed when writing programs. In addition to func­tion­al pro­gram­ming, object-oriented pro­gram­ming in Python is easy to implement.

Python has a wide range of tools for func­tion­al pro­gram­ming. However, in contrast to purely func­tion­al languages like Haskell, its scope is rather limited. The extent to which func­tion­al pro­gram­ming can be in­cor­por­ated into a Python program largely depends on the person pro­gram­ming it. Below, you’ll find an overview of the most important func­tion­al 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 cal­cu­lat­or that supports simple math­em­at­ic­al op­er­a­tions. We’ll first show the im­per­at­ive approach. This uses the classic tools in struc­tured pro­gram­ming such as con­di­tion­al branching and as­sign­ment state­ments:

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 de­clar­at­ive approach to solving the same problem. Instead of using if-branching, we can map the op­er­a­tions with Python dict. In this case, the operation symbols serve as keys that reference cor­res­pond­ing 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 de­clar­at­ive calculate function. The assert state­ments 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 ex­pres­sion with para­met­ers. Lambdas can be used every­where 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 op­er­a­tions within the function, we pass a dict with lambda functions as values. This allows us to easily add new op­er­a­tions 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 con­nec­tion with higher-order functions like map() and filter(). This allows the elements of an iterable to be trans­formed without using loops. The map() function takes a function and an iterable as para­met­ers and executes the function for each element of the iterable. Let’s consider the problem of gen­er­at­ing 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 para­met­ers 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, com­pre­hen­sions and gen­er­at­ors

Iterables are a core concept of Python. An iterable is an ab­strac­tion over col­lec­tions whose elements can be output in­di­vidu­ally. 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, com­pre­hen­sions are used. These are well suited for func­tion­al pro­gram­ming 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 eval­u­ation, which is common in purely func­tion­al languages, can be im­ple­men­ted in Python with gen­er­at­ors. 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 ex­pres­sion that cal­cu­lates 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 al­tern­at­ives are there to Python for func­tion­al pro­gram­ming?

Func­tion­al pro­gram­ming has been popular for a while and has es­tab­lished itself as a major coun­ter­force to object-oriented pro­gram­ming. The com­bin­a­tion of immutable data struc­tures with pure functions leads to code that can be easily par­al­lel­ised. Func­tion­al pro­gram­ming is par­tic­u­larly useful for the trans­form­a­tion of data into data pipelines.

Purely func­tion­al languages with strong type systems such as Haskell or the Lisp dialect Clojure are solid options. JavaS­cript is also con­sidered a func­tion­al language at its core. With TypeScript, a modern al­tern­at­ive with strong typing has been made available.

Tip

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

Go to Main Menu