Warning: This document is for the development version of Geo-Python. The main version is master.

This page was generated from source/notebooks/L3/gcp-3-pep8.ipynb.
Binder badge
Binder badge CSC badge

Good coding practices - writing readable code

This week’s session on good programming practices focuses on writing code that is easy to read. During the previous lessons we have already discussed good practices regarding variable naming and describing your code using comments, which are also part of writing readable code. Here we focus on how to format the actual Python code to make it more readable.

Working code vs readable code

As you noticed, Python forces us to indent our code when writing for loops and conditional statements. Without the indentation, the code won’t work at all, or then it will not work as you would want it to work.

However, there are many cases in which you are able to write code that runs without errors, but you (or others!) might have a hard time reading it and understanding what the code actually does.

Ideally, our Python code would be understandable both for the computer and for humans reading it. Coding conventions are a set of generally agreed ways of writing programming code in a spesific programming language. Coding conventions help programmers to write code that is consistent and easy to read. Consistency and readability are important for sharing your code with others, and also for helping your own brain to follow along!

xkcd: Code Quality https://xkcd.com/1513/

PEP 8 Style Guide

“Readability counts”

/ The Zen of Python

The PEP 8 Style Guide for Python Code gives coding conventions that help us write code that is readable (by humans!) and consistent with code written by others.

PEP 8 goes far beyond the scope of what we have learned so far during this course, and we recommend that you re-visit the guidelines every now and then when learning new things. Here, we summarize some highlights that you can start applying to your code right away!

Maximum line length

PEP 8 guides us to limit all lines to max 79 characters: https://www.python.org/dev/peps/pep-0008/#maximum-line-length. Comments (multi-line or single line) should be limited to 72 characters.

One of the guiding principles of Python is that Simple is better than complex, but sometimes you might end up having a line of code that exceeds 79 characters, for example, when defining lists.

Python is able to interpret the code correctly from multiple lines within parentheses, brackets and braces:

[1]:
# Implicit line continuation inside brackets
us_cities = ['Detroit', 'Chicago', 'Denver', 'Boston',
            'Portland', 'San Francisco', 'Houston', 'Orlando',]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/opt/python/3.8.0/lib/python3.8/codeop.py in __call__(self, source, filename, symbol)
    131
    132     def __call__(self, source, filename, symbol):
--> 133         codeob = compile(source, filename, symbol, self.flags, 1)
    134         for feature in _features:
    135             if codeob.co_flags & feature.compiler_flag:

TypeError: required field "type_ignores" missing from Module

Note: backslash (\) might be required to break a line when using more complicated statements such as the with statement (not covered during this course). See more examples in here.

Indentation

Indentation is an essential part of the Python code lay-out. As we already learned, for-loops and conditional statements won’t work correctly without indentation. PEP 8 advices us to use 4 spaces per indentation level.

Let’s have a look at our example with if-statements. The indented line tells Python what to do if the condition is True. Notice the 4 spaces in the indentation:

[2]:
weather = 'Rain'
wind = 'Windy'

if (weather == 'Rain') and (wind == 'Windy'):
    print('Just stay at home')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/opt/python/3.8.0/lib/python3.8/codeop.py in __call__(self, source, filename, symbol)
    131
    132     def __call__(self, source, filename, symbol):
--> 133         codeob = compile(source, filename, symbol, self.flags, 1)
    134         for feature in _features:
    135             if codeob.co_flags & feature.compiler_flag:

TypeError: required field "type_ignores" missing from Module

Following PEP 8, it is also possible to break the conditional expression into multiple lines if needed. Notice the extra parenheses:

[3]:
if ((weather == 'Rain')
    and (wind == 'Windy')):
    print('Just stay at home')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/opt/python/3.8.0/lib/python3.8/codeop.py in __call__(self, source, filename, symbol)
    131
    132     def __call__(self, source, filename, symbol):
--> 133         codeob = compile(source, filename, symbol, self.flags, 1)
    134         for feature in _features:
    135             if codeob.co_flags & feature.compiler_flag:

TypeError: required field "type_ignores" missing from Module

To increase readability of this if-statement, we could add extra indetation to the continuation line of the conditional statement:

[4]:
if ((weather == 'Rain')
        and (wind == 'Windy')):
    print('Just stay at home')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/opt/python/3.8.0/lib/python3.8/codeop.py in __call__(self, source, filename, symbol)
    131
    132     def __call__(self, source, filename, symbol):
--> 133         codeob = compile(source, filename, symbol, self.flags, 1)
    134         for feature in _features:
    135             if codeob.co_flags & feature.compiler_flag:

TypeError: required field "type_ignores" missing from Module

In our case, the first option with the conditional expression on one line is ok, as it is not that long afterall.

In addition, indentation is needed when breaking one command into multiple lines, such as in our example with the list us_cities above, where we used the implied line continuation inside the brackets. Following PEP 8 indentation guidelines, we can define us_cities also using a hanging indent. Note that there is no value on the first line of the list, and the closing bracket is lined up with the last line of the list:

[5]:
# Hanging indentation:
us_cities = [
            'Detroit', 'Chicago',
            'Denver', 'Boston',
            'Portland', 'San Francisco',
            'Houston', 'Orlando',
            ]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/opt/python/3.8.0/lib/python3.8/codeop.py in __call__(self, source, filename, symbol)
    131
    132     def __call__(self, source, filename, symbol):
--> 133         codeob = compile(source, filename, symbol, self.flags, 1)
    134         for feature in _features:
    135             if codeob.co_flags & feature.compiler_flag:

TypeError: required field "type_ignores" missing from Module

We will discuss more about indentations during week 4 when defining functions :)

Whitespace and binary operators

Surround binary operators with single space on either side. https://www.python.org/dev/peps/pep-0008/#other-recommendations

Do this always with:

  • assignment (=)
  • augmented assignment (+=, -= etc.)
  • comparisons (==, <, >, !=, <>, <=, >=, in, not in, is, is not)
  • Booleans (and, or, not)
[6]:
# yes:
i = 1
i = i + 1
i += 1
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/opt/python/3.8.0/lib/python3.8/codeop.py in __call__(self, source, filename, symbol)
    131
    132     def __call__(self, source, filename, symbol):
--> 133         codeob = compile(source, filename, symbol, self.flags, 1)
    134         for feature in _features:
    135             if codeob.co_flags & feature.compiler_flag:

TypeError: required field "type_ignores" missing from Module
[7]:
# no:
i=1
i=i+1
i +=1
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/opt/python/3.8.0/lib/python3.8/codeop.py in __call__(self, source, filename, symbol)
    131
    132     def __call__(self, source, filename, symbol):
--> 133         codeob = compile(source, filename, symbol, self.flags, 1)
    134         for feature in _features:
    135             if codeob.co_flags & feature.compiler_flag:

TypeError: required field "type_ignores" missing from Module

If using operators with different priorities, you can also do this:

[8]:
# yes:
a = 1
b = 2
c = (a+b) * (a-b)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/opt/python/3.8.0/lib/python3.8/codeop.py in __call__(self, source, filename, symbol)
    131
    132     def __call__(self, source, filename, symbol):
--> 133         codeob = compile(source, filename, symbol, self.flags, 1)
    134         for feature in _features:
    135             if codeob.co_flags & feature.compiler_flag:

TypeError: required field "type_ignores" missing from Module

Avoid extraneous whitespace

Avoid having a space between the function name and parenthesis when calling a function. https://www.python.org/dev/peps/pep-0008/#whitespace-in-expressions-and-statements

[9]:
# yes:
print("Hello")
Hello
[10]:
# no:
print ("Hello")
Hello

Write one statement per line

Avoid writing multiple statements on the same line: https://www.python.org/dev/peps/pep-0008/#other-recommendations

[11]:
# yes:
print("Hello")
print("world")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/opt/python/3.8.0/lib/python3.8/codeop.py in __call__(self, source, filename, symbol)
    131
    132     def __call__(self, source, filename, symbol):
--> 133         codeob = compile(source, filename, symbol, self.flags, 1)
    134         for feature in _features:
    135             if codeob.co_flags & feature.compiler_flag:

TypeError: required field "type_ignores" missing from Module
[12]:
# no:
print("Hello"); print("world")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/opt/python/3.8.0/lib/python3.8/codeop.py in __call__(self, source, filename, symbol)
    131
    132     def __call__(self, source, filename, symbol):
--> 133         codeob = compile(source, filename, symbol, self.flags, 1)
    134         for feature in _features:
    135             if codeob.co_flags & feature.compiler_flag:

TypeError: required field "type_ignores" missing from Module
[13]:
# yes:
temperature = 17
if temperature > 25:
    print(temperature,'is greater than 25')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/opt/python/3.8.0/lib/python3.8/codeop.py in __call__(self, source, filename, symbol)
    131
    132     def __call__(self, source, filename, symbol):
--> 133         codeob = compile(source, filename, symbol, self.flags, 1)
    134         for feature in _features:
    135             if codeob.co_flags & feature.compiler_flag:

TypeError: required field "type_ignores" missing from Module
[14]:
# no:
temperature = 17
if temperature > 25: print(temperature,'is greater than 25')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/opt/python/3.8.0/lib/python3.8/codeop.py in __call__(self, source, filename, symbol)
    131
    132     def __call__(self, source, filename, symbol):
--> 133         codeob = compile(source, filename, symbol, self.flags, 1)
    134         for feature in _features:
    135             if codeob.co_flags & feature.compiler_flag:

TypeError: required field "type_ignores" missing from Module

Advanced Note

You often have to balance between code readability and code length. PEP 8 guides us to generally avoid compound statements (writing multiple statements on the same line). However, according to PEP 8, it might be sometimes ok to squeeze a short piece of code into one line, for example, with thefor -statement. Sometimes you just have to judge yourself which option makes the code more readable and go for that.

One puzzling example regarding this guideline is the use of list comprehensions when defining lists. List comprehensions are a useful approach for creating lists in a consise way. We are not covering list comprehensions during the lessons, but here is a short example from the Python documentation. Let’s have a look at two options that produce the same output:

Option A) This for loop iterates over a range, squares all values and appends them to a list:

squares = []
for x in range(10):
    squares.append(x**2)

Option B) Square all values in the range using a list comprehension:

squares = [x**2 for x in range(10)]

Both approaches are fine, and you can choose the option that you think makes the code more readable.

In some cases, list comprehensions might make your code more readable and consise. In other cases, you might end up writing an excessively long statement which is difficult to read.