Perceiving Python programming paradigms

Python supports imperative, functional, procedural, and object-oriented programming; here are tips on choosing the right one for a specific use case.
159 readers like this.
Using Python to find corrupted images

Jason van Gumster. CC BY-SA 4.0

Early each year, TIOBE announces its Programming Language of The Year. When its latest annual TIOBE index report came out, I was not at all surprised to see Python again winning the title, which was based on capturing the most search engine ranking points (especially on Google, Bing, Yahoo, Wikipedia, Amazon, YouTube, and Baidu) in 2018.

Python data from TIOBE Index

Adding weight to TIOBE's findings, earlier this year, nearly 90,000 developers took Stack Overflow's annual Developer Survey, which is the largest and most comprehensive survey of people who code around the world. The main takeaway from this year's results was:

"Python, the fastest-growing major programming language, has risen in the ranks of programming languages in our survey yet again, edging out Java this year and standing as the second most loved language (behind Rust)."

Ever since I started programming and exploring different languages, I have seen admiration for Python soaring high. Since 2003, it has consistently been among the top 10 most popular programming languages. As TIOBE's report stated:

"It is the most frequently taught first language at universities nowadays, it is number one in the statistical domain, number one in AI programming, number one in scripting and number one in writing system tests. Besides this, Python is also leading in web programming and scientific computing (just to name some other domains). In summary, Python is everywhere."

There are several reasons for Python's rapid rise, bloom, and dominance in multiple domains, including web development, scientific computing, testing, data science, machine learning, and more. The reasons include its readable and maintainable code; extensive support for third-party integrations and libraries; modular, dynamic, and portable structure; flexible programming; learning ease and support; user-friendly data structures; productivity and speed; and, most important, community support. The diverse application of Python is a result of its combined features, which give it an edge over other languages.

But in my opinion, the comparative simplicity of its syntax and the staggering flexibility it provides developers coming from many other languages win the cake. Very few languages can match Python's ability to conform to a developer's coding style rather than forcing him or her to code in a particular way. Python lets more advanced developers use the style they feel is best suited to solve a particular problem.

While working with Python, you are like a snake charmer. This allows you to take advantage of Python's promise to offer a non-conforming environment for developers to code in the style best suited for a particular situation and to make the code more readable, testable, and coherent.

Python programming paradigms

Python supports four main programming paradigms: imperative, functional, procedural, and object-oriented. Whether you agree that they are valid or even useful, Python strives to make all four available and working. Before we dive in to see which programming paradigm is most suitable for specific use cases, it is a good time to do a quick review of them.

Imperative programming paradigm

The imperative programming paradigm uses the imperative mood of natural language to express directions. It executes commands in a step-by-step manner, just like a series of verbal commands. Following the "how-to-solve" approach, it makes direct changes to the state of the program; hence it is also called the stateful programming model. Using the imperative programming paradigm, you can quickly write very simple yet elegant code, and it is super-handy for tasks that involve data manipulation. Owing to its comparatively slower and sequential execution strategy, it cannot be used for complex or parallel computations.

Linus Torvalds quote

Consider this example task, where the goal is to take a list of characters and concatenate it to form a string. A way to do it in an imperative programming style would be something like:

>>> sample_characters = ['p','y','t','h','o','n']
>>> sample_string = ''
>>> sample_string
''
>>> sample_string = sample_string + sample_characters[0]
>>> sample_string
'p'
>>> sample_string = sample_string + sample_characters[1]
>>> sample_string
'py'
>>> sample_string = sample_string + sample_characters[2]
>>> sample_string
'pyt'
>>> sample_string = sample_string + sample_characters[3]
>>> sample_string
'pyth'
>>> sample_string = sample_string + sample_characters[4]
>>> sample_string
'pytho'
>>> sample_string = sample_string + sample_characters[5]
>>> sample_string
'python'
>>>

Here, the variable sample_string is also like a state of the program that is getting changed after executing the series of commands, and it can be easily extracted to track the progress of the program. The same can be done using a for loop (also considered imperative programming) in a shorter version of the above code:

>>> sample_characters = ['p','y','t','h','o','n']
>>> sample_string = ''
>>> sample_string
>>> for c in sample_characters:
...    sample_string = sample_string + c
...    print(sample_string)
...
p
py
pyt
pyth
pytho
python
>>>

Functional programming paradigm

The functional programming paradigm treats program computation as the evaluation of mathematical functions based on lambda calculus. Lambda calculus is a formal system in mathematical logic for expressing computation based on function abstraction and application using variable binding and substitution. It follows the "what-to-solve" approach—that is, it expresses logic without describing its control flow—hence it is also classified as the declarative programming model.

The functional programming paradigm promotes stateless functions, but it's important to note that Python's implementation of functional programming deviates from standard implementation. Python is said to be an impure functional language because it is possible to maintain state and create side effects if you are not careful. That said, functional programming is handy for parallel processing and is super-efficient for tasks requiring recursion and concurrent execution.

>>> sample_characters = ['p','y','t','h','o','n']
>>> import functools
>>> sample_string = functools.reduce(lambda s,c: s + c, sample_characters)
>>> sample_string
'python'
>>>

Using the same example, the functional way of concatenating a list of characters to form a string would be the same as above. Since the computation happens in a single line, there is no explicit way to obtain the state of the program with sample_string and track the progress. The functional programming implementation of this example is fascinating, as it reduces the lines of code and simply does its job in a single line, with the exception of using the functools module and the reduce method. The three keywords—functools, reduce, and lambda—are defined as follows:

  • functools is a module for higher-order functions and provides for functions that act on or return other functions. It encourages writing reusable code, as it is easier to replicate existing functions with some arguments already passed and create a new version of a function in a well-documented manner.
  • reduce is a method that applies a function of two arguments cumulatively to the items in sequence, from left to right, to reduce the sequence to a single value. For example:
    >>> sample_list = [1,2,3,4,5]
    >>> import functools
    >>> sum = functools.reduce(lambda x,y: x + y, sample_list)
    >>> sum
    15
    >>> ((((1+2)+3)+4)+5)
    15
    >>> 
  • lambda functions are small, anonymized (i.e., nameless) functions that can take any number of arguments but spit out only one value. They are useful when they are used as an argument for another function or reside inside another function; hence they are meant to be used only for one instance at a time.

Procedural programming paradigm

The procedural programming paradigm is a subtype of imperative programming in which statements are structured into procedures (also known as subroutines or functions). The program composition is more of a procedure call where the programs might reside somewhere in the universe and the execution is sequential, thus becoming a bottleneck for resource utilization. Like the imperative programming paradigm, procedural programming follows the stateful model. The procedural programming paradigm facilitates the practice of good program design and allows modules to be reused in the form of code libraries.

This modularized form of development is a very old development style. The different modules in a program can have no relationship with each other and can be located in different locations, but having a multitude of modules creates hardships for many developers, as it not only leads to duplication of logic but also a lot of overhead in terms of finding and making the right calls. Note that in the following implementation, the method stringify could be defined anywhere in the universe and, to do its trick, only require the proper call with the desired arguments.

>>> def stringify(characters):
...    string = ''
...    for c in characters:
...        string = string + c
...    return stringify
...
>>> sample_characters = ['p','y','t','h','o','n']
>>> stringify(sample_characters)
'python'
>>> 

Object-oriented programming paradigm

The object-oriented programming paradigm considers basic entities as objects whose instance can contain both data and the corresponding methods to modify that data. The different principles of object-oriented design help code reusability, data hiding, etc., but it is a complex beast, and writing the same logic an in object-oriented method is tricky. For example:

>>> class StringOps:
...    def __init__(self, characters):
...        self.characters = characters
...    def stringify(self):
...        self.string = ''.join(self.characters)
...
>>> sample_characters = ['p','y','t','h','o','n']
>>> sample_string = StringOps(sample_characters)
>>> sample_string.stringify()
>>> sample_string.string
'python'
>>>

What programming paradigm should I choose?

It's important to note that there is no comparison between the different types of programming paradigms. Since software is nothing but knowledge representation, the answer to the question: "What is the best way to represent my problem?" is choosing a specific programming paradigm.

In layman's terms, if your problem involves a series of simple sequential manipulations, following the old-school imperative programming paradigm would be the least expensive in terms of time and effort and give you the best results. In the case of problems requiring mathematical transformations of values, filtering information, mapping, and reductions, functional programming with program computation as mathematical functions might come in handy. If the problem is structured as a bunch of interrelated objects with certain attributes that can change with the passage of time, depending on certain conditions, object-oriented programming will be super-useful. Of course, a rule-based approach would not work here, as programming paradigm choice is also heavily dependent on the type of data to be processed, the dynamic needs of the system, and various other things like scalability.

Analyzing the latest tech buzzwords can help identify why certain programming paradigms work better than others.

  • Machine learning uses a healthy mix of imperative programming and functional programming with a dash of immutability. Feature extraction and preprocessing are best approached functionally, as they require mathematical processing of data because mappings, reductions, and filtrations can pretty much be done in parallel without much dependence on each others' data points. Training of machine learning models is best approached via old-school imperative programming, as optimizing functions' value (a.k.a. the state of the program) needs to be updated at each iteration and therefore requires sequential execution at many points in the algorithm. It is quicker than functional programming in that case. It also avoids creating copies of everything after each step; instead it just updates the previous-value placeholders.
  • Deep learning can be performed well in a functional manner, as deep learning models are compositional. The entire process optimizes a set of composite functions, weights are immutable and stateless, and updates can be applied in any order as long as corresponding inputs are computed. Using functional programming provides concurrency and parallelism at no cost and also makes it easier to work with large, distributed models. There are also certain custom paradigms where functional programming is intertwined with informational theory to avoid overfitting in the statistical models.
  • Data manipulation can be approached with either functional or object-oriented programming. In the functional programming way, everything is immutable, algorithms are expressed succinctly, and there is native pattern matching, but formulation of the mathematical expression-like command is an art. Approaching it in an object-oriented programming way provides for recursive and iterative loops and a class-based structure that makes it easier to scale for bigger data and new functions. The downside is that the algorithms and the code logic are not expressed in a readable way. Although both paradigms tend to have an automatic garbage-collection system and can access and manipulate databases smoothly, the choice of which one to choose is heavily dependent on the programmer's knowledge.

Takeaway

There is a high probability that any two developers would disagree on the best coding style for any situation and have valid arguments to support their opinion. The amazing thing about Python is that it lets you choose the programming paradigm that works the best for you in a given situation.

As the above examples demonstrate, a task can always be broken into sub-tasks where each smaller part is coded in a completely different paradigm. The mix-and-match style works perfectly as long as the packages used are minimal, the inputs and outputs are clearly defined, and the complexity is moderated. There are no rules that say you can’t combine styles as needed. Python doesn’t stop in the middle of interpreting your application and display a style error when you mix styles.

Because there is no perfect guidebook for choosing a correct coding style for a given use case, the best suggestion is to try several paradigms weighing in their pros and cons until you find the one that leads to a simple yet efficient solution. There will be times during this experimentation when you will see that instead of using a single style throughout, a combination of programming paradigms works better for different parts to a solution. During this process, it is also highly recommended to document the requirements and the trials of different styles to share with the community and get feedback. The comments and suggestions will help with development as well as your teammates and any future developers that are added to the team.


Jigyasa Grover presented Taming styles of Python programming at All Things Open, October 13-15 in Raleigh, N.C.

What to read next
Tags

2 Comments

Whatever style of programming you use, you should try to make debugging as easy as possible. In particular, you want a structure that allows you to come back 6 months or a year later and quickly understand how the program works.

The stringify() function of the procedural version should end with "return string" (not "return stringify").

Creative Commons LicenseThis work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.