Python Good Practices Part 2 - Writing Pythonic code

While reading top-shelf Python books, you probably might have bumped into the sentence "be pythonic". But what exactly does it mean and how to use it in real life examples? This article presents some practical cases where pythonic approach should be used and shows you some code snippets, where this approach was implemented.

|
   Python 

Python is appreciated for its clarity, easy usage and fast-to-learn syntax. However, when someone migrates from another language, the coding habits also migrates. Best solutions in other languages probably will work in Python as well but will not be recognized as best practice. Using Python in the way it was designed for makes your code more pythonic.

To have better understanding of the Python’s design principles, import ‘this’ module and learn the Zen of Python.

Note: This is not the scope of this article but first good practice is to get familiar with the PEP-8 – Style Guide for Python and make the rules given there as a habit while writing Python code. https://www.python.org/dev/peps/pep-0008/

1. General

Besides the obvious datatytes such as int, float, Boolean and string, Python has also several default collection datatypes e.g. list, tuple, set or dictionary.

tuple_ = (1, 2, 3)
list_ = ['a', 'b', 'c']
set_ = {'one', 'two', 'three'}
dict_ = {1: 'a', 2: 'b', 3: 'c'}

Python has also many built-in functions that can make our code more readable and faster as well. These built-in functions can be found here: https://docs.python.org/3.3/library/functions.html

Moreover, Python has a rich Standard Library with very useful modules such as collections, functools, decimal, itertools, os, sys, re, random, time etc. If you would like to know more, click the link here.

-        Assigning multiple values to variables

The common example would be swapping two variables. It is usually done with the temporary variable usage:

x = 10
y = 100
temp_var = x
x = y
y = temp_var

But in Python we can assign multiple variables in one line e.g.:

x, y = 10, 100
x, y = y, x

Thanks to this we do not need to create additional temporary value, i.e.:

x, y = 10, 100
x, y = y, x+y

This will result with x = 100 and y = 110.

-        Unpacking collection to multiple variables

The conventional way of assigning collection elements to specific variables require list indexation e.g.:

actor_info = ['Barney Stinson', 'Neil Patrick Harris', 'How I Met Your Mother', 1975]

name = actor_info[0]
actor = actor_info[1]
series = actor_info[2]

But Python provides the possibility to unpack values of collection straight to the variables e.g:

actor_info = ['Barney Stinson', 'Neil Patrick Harris', 'How I Met Your Mother', 1975]
name, actor, series, _ = actor_info

-        String concatenation

We probably would like to concatenate string with += operator e.g.:

countries_population = {
    "Afghanistan": 22720000,
    "Albania": 3401200,
    "Andorra": 78000,
    "Luxembourg": 435700,
    "Montserrat": 11000,
    "United Kingdom": 59623400,
    "United States": 278357000,
    "Zimbabwe": 11669000
}

countries_population_str = ''

for country, population in countries_population.items():
    countries_population_str += 'Country {} has population of {}\n'.format(country, population)

However, we should use join() method instead. It creates string concatenated from the iterable. With the generator expression we can simplify the code to:

countries_population_str = '\n'.join('Country {} has population {}'.format(country, population)
                                     for country, population in countries_population.items())
2. Creating collections

While writing in Python you might need to create a collection from another one. We can do this easily and idiomatic with built-in functions, comprehensions or unpacking values straight to the constructor.

-        Creating list from another collection

We can create collection by iterating every element from another one:

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
multiplied_numbers = []
for number in numbers:
    multiplied_numbers.append(number*2)

But flat is better than nested. Instead we should use list comprehensions:

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
multiplied_numbers = [number * 2 for number in numbers]

We can do the same with any other collections like tuple, set or dictionary.

-        Creating dictionary from two lists:

We can create an empty dictionary and assign key-value pair e.g.:

keys = [1, 2, 3, 4]
values = ['Matthew', 'Brad', 'Betty', 'Evelyn']

result = {}
for key, value in zip(keys, values):
    result[key] = value

Instead we should take benefit of the dict() constructor that gets key-value pair. It can be written this way:

keys = [1, 2, 3, 4]
values = ['Matthew', 'Brad', 'Betty', 'Evelyn']

result = dict(zip(keys, values))

-        Creating dictionary and modifying values (if key exists)

Sometimes you may need to update dictionary values dynamically without the knowledge of key existence. In this situation we could check every time whether the key exists. Let’s consider the case, when we want to create dictionary where keys are elements from list and values are number of occurrences in this list e.g.:

numbers = [1, 2, 3, 1, 2, 3, 1, 2, 4]
occurrences_dict = {}
for number in numbers:
    if number not in occurrences_dict:
        occurrences_dict[number] = 0
    occurrences_dict[number] += 1

However, dictionary has method get() that takes the key and the default value that should be assigned to the key if it does not exist:

numbers = [1, 2, 3, 1, 2, 3, 1, 2, 4]
occurrences_dict = {} 
for number in numbers:
    occurrences_dict[number] = occurrences_dict.get(number, 0) + 1

We can also use defaultdict from collections module. It takes the type as a parameter. Every time we refer the key that does not exist it would be created with the default value of the provided type.

from collections import defaultdict
numbers = [1, 2, 3, 1, 2, 3, 1, 2, 4]
occurrences_dict = defaultdict(int)
for number in numbers:
    occurrences_dict[number] += 1

This example shows us different ways to create a dictionary with occurrences of the list’s elements. With the familiarity of collections module it can be even more simplified:

from collections import Counter

numbers = [1, 2, 3, 1, 2, 3, 1, 2, 4]
occurrences_dict = dict(Counter(numbers))

As we can see, the goal can be achieved in different ways. The more the code follows the Python design principles, the better.

3. Looping

Looping is essential feature of Python. Its syntax differs from other languages. Its usage is very intuitive. We can iterate over default Python collections datatypes and every iterable or generator objects.

-        Looping over a collection

The conventional way of manipulating with every element of the list/array in e.g. Java would look like this:

String[] names = {"Arthur", "Betty", "Matthew", "Brad"};

for (int i=0; i<names.length(); i++){
    do_something(names[i]);
}

The equivalent in Python would be:

names = ['Arthur', 'Betty', 'Matthew', 'Brad']

i = 0
while i < len(names):
    do_something(names[i])
    i += 1

Or even simpler using the range function:

names = ['Arthur', 'Betty', 'Matthew', 'Brad']

for index in range(len(names)):

But neither of the above examples would be stated as Pythonic. Instead we should use core looping idiom i.e.:

names = ['Arthur', 'Betty', 'Matthew', 'Brad']

for name in names:
    do_something(name)

It is not only more elegant and more readable but also faster. The good practice is to get rid of index manipulation in our loops as often as possible.

-        Iterating over list with elements’ indexes

The common practice of getting the index of an element in a list while iterating is to create additional index counter and increment it in every iteration:

names = ['Arthur', 'Betty', 'Matthew', 'Brad']
index = 0
for name in names:
    print('%i -> %s' % (index, name))
    index += 1

But Python provides the built-in function enumerate() that returns list’s elements with their indexes. It is more readable and concise:

for index, name in enumerate(names):
    print('%i -> %s' % (index, name))

-        Iterating over two merged lists

If we want to get elements from two lists with the same index we would probably use the code below:

names = ['Arthur', 'Betty', 'Matthew', 'Brad']
numbers = [1, 2, 3]
for index, name in enumerate(names):
    if index < len(numbers):
        print('%s -> %s' % (name, numbers[index]))

However, the zip() built-in function can be used instead. It merges two lists of a minimum length from those lists and returns elements in the same index:

names = ['Arthur', 'Betty', 'Matthew', 'Brad']
numbers = [1, 2, 3]
for name, number in zip(names, numbers):
    print('%s -> %s' % (name, number))

-        Iterating over reversed collection

Taking into consideration the ‘for’ loop syntax in another languages, we would probably like to write this in the following way:

names = ['Barney', 'Ted', 'Marshall', 'Lily', 'Robin']

for index in range(len(names)-1, -1, -1):
    print(names[index])

But Python has reversed() built-in function:

names = ['Barney', 'Ted', 'Marshall', 'Lily', 'Robin']

for name in reversed(names):
    print(name)
4. Checking occurrences in collections

Very often we need to check whether specific element exists in a collection. Python has very useful constructions for it.

-        Checking occurrence of element in list

If we want to check the occurrence of the specific element in a list we probably would like to iterate over the list and check every element at a time

countries = ['Albania', 'Austria', 'Chile', 'China', 'Poland']

def is_country(country):
    for country_ in countries:
        if country_ == country:
            return True

However, Python has ‘in’ keyword in conditional statements that is doing exactly the same thing i.e. check the occurrence of an element in a collection:

countries = ['Albania', 'Austria', 'Chile', 'China', 'Poland']

def is_country(country):
    return country in countries

-        Checking multiple occurrences in list

In case we would like to check multiple elements whether they exist in a collection we could also use ‘in’ keyword in conditional statement:

countries = ['Albania', 'Austria', 'Chile', 'China', 'Poland']

def are_countries(*country):
    for country_ in country:
        if country_ not in countries:
            return False
    return True

But there is a built-in function all() that simplify this. It checks whether all elements in an iterable are True (boolean True, not empty list, non-zero integer/float etc.)

countries = ['Albania', 'Austria', 'Chile', 'China', 'Poland']

def are_countries(*country):
    return all(country_ in countries for country_ in country)

Python has also similar built-in function any() that checks whether any of the elements in an iterable results to True.

5. Switch statement

Although Python does not support a switch statement we are able to overcome this.  Very often we can encounter the solution containing multiple if..elif statements i.e.:

from enum import Enum

Shape = Enum('Shape', 'CIRCLE TRIANGLE SQUARE LINE')

def draw_circle():
    print('Drawing circle')

def draw_triangle():
    print('Drawing triangle')

def draw_square():
    print('Drawing square')

def draw_line():
    print('Drawing line')

def draw(shape):
    if shape is Shape.CIRCLE:
        draw_circle()
    elif shape is Shape.TRIANGLE:
        draw_triangle()
    elif shape is Shape.SQUARE:
        draw_square()
    elif shape is Shape.LINE:
        draw_line()
    else:
        draw_line()

draw(Shape.TRIANGLE)

But flat is better than nested. With the power of dictionary we can simplify this in the following way:

draw_options = {
    Shape.CIRCLE: draw_circle,
    Shape.TRIANGLE: draw_triangle,
    Shape.SQUARE: draw_square,
    Shape.LINE: draw_line
}

def draw(shape):
    draw_options.get(shape, Shape.LINE)()

draw(Shape.TRIANGLE)

We handled the problem without any ‘if’ statement. The code is more readable.  Now we can see why there is no need for switch statement in Python. This solution perfectly shows the meaning of Pythonic word.

Summary

Python is sometimes called as ‘executable pseudocode’. It is hard to disagree when we see some of its syntax and constructs. Its design gives us the possibility to write code very readable, beautiful and simple. Many algorithms can be written in various ways and the result will be the same. It is up to the programmer which one he chooses. However Python design should drive us to write code in specific way that could be stated as Pythonic. The presented examples in this article are just a top of an iceberg.  The practice is essential to dive into Python.

The Zen of Python:

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Szymon Piwowar

Did you like this article?

Python Good Practices Part 2 - Writing Pythonic code