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!
PEP 8 Style Guide¶
“Readability counts”
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
- Indentation
- Whitespace and binary operators
- Avoid extraneous whitespace
- Write one statement per line
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.