Functional Programming in Python
Functional Programming is a popular programming paradigm closely linked to computer science's mathematical foundations. While there is no strict definition of what constitutes a functional language, we consider them to be languages that use functions to transform data. Python is not a functional programming language but it does incorporate some of its concepts alongside other programming paradigms. With Python, it's easy to write code in a functional style, which may provide the best solution for the task at hand.
Functional Programming Concepts
Functional languages are declarative languages, they tell the computer what result they want. This is usually contrasted with imperative languages that tell the computer what steps to take to solve a problem. Python is usually coded in an imperative way but can use the declarative style if necessary. Some of Python's features were influenced by Haskell, a purely functional programming language. To get a better appreciation of what a functional language is, let's look at features in Haskell that can be seen as desirable, functional traits:
- Pure Functions - do not have side effects, that is, they do not change the state of the program. Given the same input, a pure function will always produce the same output.
- Immutability - data cannot be changed after it is created. Take for example creating a
Listwith 3 items and storing it in a variable
my_listis immutable, you wouldn't be able to change the individual items. You would have to set
my_listto a new
Listif you'd like to use different values.
- Higher Order Functions - functions can accept other functions as parameters and functions can return new functions as output. This allows us to abstract over actions, giving us flexibility in our code's behavior.
Haskell has also influenced iterators and generators in Python through its lazy loading, but that feature isn't necessary for a functional language.
Functional Programming in Python
Without any special Python features or libraries, we can start coding in a more functional way.
If you'd like functions to be pure, then do not change the value of the input or any data that exists outside the function's scope. This makes the function we write much easier to test. As it does not change the state of any variable, we are guaranteed to get the same output every time we run the function with the same input. Let's create a pure function to multiply numbers by 2:
def multiply_2_pure(numbers): new_numbers =  for n in numbers: new_numbers.append(n * 2) return new_numbers original_numbers = [1, 3, 5, 10] changed_numbers = multiply_2_pure(original_numbers) print(original_numbers) # [1, 3, 5, 10] print(changed_numbers) # [2, 6, 10, 20]
The original list of
numbers are unchanged, and we don't reference any other variables outside of the function, so it is pure.
Ever had a bug where you wondered how a variable you set to 25 became
None? If that variable was immutable, the error would have been thrown where the variable was being changed, not where the changed value already affected the software - the root cause of the bug can be found earlier.
Python offers some immutable data types, a popular one being the
Tuple. Let's contrast the Tuple to a List, which is mutable:
mutable_collection = ['Tim', 10, [4, 5]] immutable_collection = ('Tim', 10, [4, 5]) # Reading from data types are essentially the same: print(mutable_collection) # [4, 5] print(immutable_collection) # [4, 5] # Let's change the 2nd value from 10 to 15 mutable_collection = 15 # This fails with the tuple immutable_collection = 15
The error you would see is:
TypeError: 'tuple' object does not support item assignment.
Now, there's an interesting scenario where a
Tuple may appear to be a mutable object. For instance, if we wanted to change the list in
[4, 5] to
[4, 5, 6], you can do the following:
immutable_collection.append(6) print(immutable_collection) # [4, 5, 6]
This works because a
List is a mutable object. Let's try to change the list back to
immutable_collection = [4, 5] # This throws a familiar error: # TypeError: 'tuple' object does not support item assignment
It fails just as we expected it to. While we can change the contents of a mutable object in a
Tuple, we cannot change the reference to the mutable object that's stored in memory.
Higher Order Functions
Recall that Higher Order Functions either accept a function as an argument or return a function for further processing. Let's illustrate how simple both can be created in Python. Consider a function that prints a line multiple times:
def write_repeat(message, n): for i in range(n): print(message) write_repeat('Hello', 5)
What if we wanted to write to a file 5 times, or log the message 5 times? Instead of writing 3 different functions that all loop, we can write 1 Higher Order Function that accepts those functions as an argument:
def hof_write_repeat(message, n, action): for i in range(n): action(message) hof_write_repeat('Hello', 5, print) # Import the logging library import logging # Log the output as an error instead hof_write_repeat('Hello', 5, logging.error)
Now imagine that we're tasked with creating functions that increment numbers in a list by 2, 5, and 10. Let's start with the first case:
def add2(numbers): new_numbers =  for n in numbers: new_numbers.append(n + 2) return new_numbers print(add2([23, 88])) # [25, 90]
While it's trivial to write
add10 functions, it's obvious that they would operate in the same: looping through the list and adding the incrementer. So instead of creating many different increment functions, we create 1 Higher Order Function:
def hof_add(increment): # Create a function that loops and adds the increment def add_increment(numbers): new_numbers =  for n in numbers: new_numbers.append(n + increment) return new_numbers # We return the function as we do any other value return add_increment add5 = hof_add(5) print(add5([23, 88])) # [28, 93] add10 = hof_add(10) print(add10([23, 88])) # [33, 98]
Higher Order Functions give our code flexibility. By abstracting what functions are applied or returned, we gain more control of our program's behavior. Python provides some useful built-in Higher Order Functions, which makes working with sequences much easier. We'll first look at lambda expressions to better utilize these built-in functions.
A lambda expression is an anonymous function. When we create functions in Python, we use the
def keyword and give it a name. Lambda expressions allow us to define a function much more quickly.
Let's create a Higher Order Function
hof_product that returns a function that multiplies a number by a predefined value:
def hof_product(multiplier): return lambda x: x * multiplier mult6 = hof_product(6) print(mult6(6)) # 36
The lambda expression begins with the keyword
lambda followed by the function arguments. After the colon is the code returned by the lambda. This ability to create functions "on the go" is heavily used when working with Higher Order Functions.
There's a lot more to lambda expressions that we cover in our article Lambda Functions in Python if you want more info.
Built-in Higher Order Functions
Python has implemented some commonly used Higher Order Functions from Functional Programming Languages that makes processing iterable objects like lists and iterators much easier. For space/memory efficiency reasons, these functions return an
iterator instead of a list.
map function allows us to apply a function to every element in an iterable object. For example, if we had a list of names and wanted to append a greeting to the Strings, we can do the following:
names = ['Shivani', 'Jason', 'Yusef', 'Sakura'] greeted_names = map(lambda x: 'Hi ' + x, names) # This prints something similar to: <map object at 0x10ed93cc0> print(greeted_names) # Recall, that map returns an iterator # We can print all names in a for loop for name in greeted_names: print(name)
filter function tests every element in an iterable object with a function that returns either
False, only keeping those which evaluates to
True. If we had a list of numbers and wanted to keep those that are divisible by 5 we can do the following:
numbers = [13, 4, 18, 35] div_by_5 = filter(lambda num: num % 5 == 0, numbers) # We can convert the iterator into a list print(list(div_by_5)) # 
As each function returns an iterator, and they both accept iterable objects, we can use them together for some really expressive data manipulations!
# Let's arbitrarily get the all numbers divisible by 3 between 1 and 20 and cube them arbitrary_numbers = map(lambda num: num ** 3, filter(lambda num: num % 3 == 0, range(1, 21))) print(list(arbitrary_numbers)) # [27, 216, 729, 1728, 3375, 5832]
The expression in
arbitrary_numbers can be broken down to 3 parts:
range(1, 21)is an iterable object representing numbers from 1, 2, 3, 4... 19, 20.
filter(lambda num: num % 3 == 0, range(1, 21))is an iterator for the number sequence 3, 6, 9, 12, 15 and 18.
- When they're cubed by the
mapexpression we can get an iterator for the number sequence 27, 216, 729, 1728, 3375 and 5832.
A popular Python feature that appears prominently in Functional Programming Languages is list comprehensions. Like the
filter functions, list comprehensions allow us to modify data in a concise, expressive way.
Let's try our previous examples with
filter with list comprehensions instead:
# Recall names = ['Shivani', 'Jan', 'Yusef', 'Sakura'] # Instead of: map(lambda x: 'Hi ' + x, names), we can do greeted_names = ['Hi ' + name for name in names] print(greeted_names) # ['Hi Shivani', 'Hi Jason', 'Hi Yusef', 'Hi Sakura']
A basic list comprehensions follows this format: [result
If we'd like to filter objects, then we need to use the
# Recall numbers = [13, 4, 18, 35] # Instead of: filter(lambda num: num % 5 == 0, numbers), we can do div_by_5 = [num for num in numbers if num % 5 == 0] print(div_by_5) #  # We can manage the combined case as well: # Instead of: # map(lambda num: num ** 3, filter(lambda num: num % 3 == 0, range(1, 21))) arbitrary_numbers = [num ** 3 for num in range(1, 21) if num % 3 == 0] print(arbitrary_numbers) # [27, 216, 729, 1728, 3375, 5832]
filter expression can be expressed as a list comprehension.
Some Things to Consider
It's well known that the creator of Python, Guido van Rossum, did not intend for Python to have functional features but did appreciate some of the benefits its introduction has brought to the language. He discussed the history of Functional Programming language features in one of his blog posts. As a result, the language implementations have not been optimized for Functional Programming features.
Furthermore, the Python developer community does not encourage using the vast array of Functional Programming features. If you were writing code for the global Python community to review, you would write list comprehensions instead of using
filter. Lambdas would be used minimally as you would name your functions.
In your Python interpreter, enter
import this and you will see "The Zen of Python". Python generally encourages code to be written in the most obvious way possible. Ideally, all code should be written in one way - the community doesn't think it should be in a Functional style.
Functional Programming is a programming paradigm with software primarily composed of functions processing data throughout its execution. Although there's not one singular definition of what is Functional Programming, we were able to examine some prominent features in Functional Languages: Pure Functions, Immutability, and Higher Order Functions.
Python allows us to code in a functional, declarative style. It even has support for many common functional features like Lambda Expressions and the
However, the Python community does not consider the use of Functional Programming techniques best practice at all times. Even so, we've learned new ways to solve problems and if needed we can solve problems leveraging the expressivity of Functional Programming.