A computer program is read more often than it's written.

This is a pearl of well-established wisdom in the programming community.

Can you think of any reasons why it is so?

To understand why it is so, we can think of a program that needs to be updated to include additional functionalities. To write new or additional code, you will have to read the previous code to assess the impact of the new code on existing functionalities. Therefore, programs that are easy to read makes additional code easy to write and maintain.

We have covered quite a bit of Python so far. What is the one feature you like the most?

One of the most important features Python programmers like about Python is its high readability. And one reason why Python code is highly readable is due to the widely accepted and followed Code Style Guidelines, conventions, and idioms in the Python community.

What do you think are code style guidelines?
Code style guidelines are a set of rules or guidelines used when writing the source code for a computer program.

In earlier chapters, we have encountered numerous such conventions or recommended way of doing things in Python. In this chapter, we will take a look in detail.

Code Style Guidelines

So far, we have written very simple scripts in Python. To build something complex, such as a text editor, a computer game, or an Operating System, you will require a group of programmers who can work together to develop a program. However, individual programmers have their way of doing things. For instance, a programmer might like to write names in camel_case while another might think snakeCase is much better.

When both of these programmers work together, they might end up with inconsistent naming styles, such as shown below.

userName = "Luffy"			# Snake Case
user_id = 102				# Camel Case
has_user_enrolled = False	# Camel Case
isUserMember = True			# Snake Case
is_user_admin = True		# Camel Case

The naming style doesn't affect program execution; however, inconsistent naming styles leads to poor code readability.

Let's see how does the code looks when everything is in the same naming style.

user_name = "Luffy"
user_id = 102
has_user_enrolled = False
is_user_member = True
is_user_admin = True

You can see that having consistent naming styles is easier to read and follow through than those with inconsistent naming styles.

Naming styles are among many avenues in programming, which could lead to inconsistent written code. This could lead to less readable code. Other avenues include function doc-strings styles, inline comments, module importing styles and, maximum line length. Therefore, while working in teams, programmers define a style guide that every team member can refer to write code in a consistent style.

How does a style guide help in a programming project?

  1. Helps write code in a consistent style, which is easier to read code
  2. Automatically detects a bug
  3. Gives the feeling of doing a lot when you are doing little
  4. Helps in contributing to world peace

A style guide is all about consistency.

Using a style guide in a project helps suppress individual programmers' tendency to write things in their way. Let's take a look at the official style guide of Python.

We have already seen many standard modules built-in Python. Because Python is an open-source project, thousands of programmers contribute pieces of code to make it more useful. To make it easier for every participating open-source contributor to write consistent code, Python has a well-defined style guide.

This style guide was developed after a lot of discussion among the core contributors via a Python Enhancement Proposal (PEP). PEP is how the contributors propose new changes to the Python programming language.

The following defines the importance of a PEP.

PEP stands for Python Enhancement Proposal. A PEP is a design document providing information to the Python community or describing a new feature for Python or its processes or environment.
Whenever particular new features are added, or existing features are modified in Python, a detailed proposal is put forward and discussed. Once a consensus is reached, the community moves to implement the proposal if accepted.

The Python style guide is popularly referred to as PEP - 8.

PEP 8 (also spelled PEP8 or PEP-8) is a document that provides guidelines and best practices on how to write Python code. The entire Python community does its best to adhere to the guidelines laid out within this document. Though, some projects may sway from it from time to time.

We will not be covering the whole PEP8 document. Rather we will take a look at some of the important style guidelines that PEP8 suggests.

We can classify the guidelines into the following groups:

  • Naming Conventions
  • Code Layout
  • Indentation
  • Comments
  • White Space

We will start with Naming Conventions.

Naming Conventions

Let me ask you a question. How does naming conventions help in writing readable code?

While programming using Python, you will have to name many different objects, such as identifiers, functions, or modules. Choosing readable and reasonable names will save you time and effort later on. Writing better names helps in figuring out what a function or identifier does from the name itself.

Table 1 shows different naming conventions prescribed in the PEP8.

Table 1: PEP8 Naming conventions
Object Naming Convention Examples
Identifiers Use a lowercase single letter, word, or words. Separate words with underscores to improve readability. y, customer_age
Function Function names should be lowercase, with words separated by underscores as necessary to improve readability. detect_age, greet
Method Use the function naming rules: lowercase with words separated by underscores as necessary to improve readability. method , my_method
Class Class names should normally use the CapWords convention. MyClass
Module Modules should have short, all-lowercase names. Underscores can be used in the module name if it improves readability. module.py, my_module.py
Package Python packages should have short, all-lowercase names, although the use of underscores is discouraged. getsum, mypackage
Constant Constants are usually defined on a module level and written in all capital letters with underscores separating words CONSTANT, MY_CONSTANT

A constant is an identifier whose value is not changed in the program. You cannot create constants in Python, but if you name an identifier with all capital letters, it is assumed by other developers that it is a constant.

Another thing to keep in mind is the names to avoid.

Never use the characters l (lowercase letter L), O (uppercase letter O), or I (uppercase letter I) as single-character variable names. In many fonts, these characters are indistinguishable from the numerals one and zero. This can lead to confusion. When you need to use l, use L instead.

Let's do an exercise.

In the following piece of code, which object doesn't follow PEP 8 guidelines regarding naming conditions.

LOWER_AGE_LIMIT = 18
UPPER_AGE_LIMIT = 25

def CheckValidAge(age):
    age_condition = LOWER_AGE_LIMIT < age < UPPER_AGE_LIMIT
    return "Valid age" if age_condition else "Invalid age"
  1. Function CheckValidAge()
  2. Constant LOWER_AGE_LIMIT
  3. Identifier age_condition
  4. Class CheckValidAge()

Code Layout

How you structure your code also determines its readability. The following are some of the guidelines to create more readable code.

Maximum Line Length

Limit all lines to a maximum of 79 characters.

Programmers often work with multiple files open side-by-side. Limiting long lines of text to 79 characters enables them to easily review code. The preferred way of wrapping long lines is by using Python’s implied line continuation inside parentheses, brackets, and braces.

For instance, let's say you have an if statement with a long list of conditions.

if (this_is_first_cond and this_is_second_condition and this_is_third_condition and this_is_fourth_condition):
    # do_something()

As codes inside parentheses follow implicit line continuation, we can wrap them as follows.

if (this_is_first_cond
    	and this_is_second_condition
    	and this_is_third_condition
    	and this_is_fourth_condition):
    # do_something()

You can also use backslashes wherever it seems appropriate. For instance,

with open('/really/long/file/path/read') as file_1, \
     open('/really/long/file/path/write', 'w') as file_2:
    file_2.write(file_1.read())

However, you should mostly use the implicit line continuation using parentheses whenever possible. For instance, below is an example of using parentheses in the place of backslash (\).

# Wrong:
from deep.module.inside.package import some_function, some_other_function, \
    and_some_other_function

# Correct:
from deep.module.inside.package import (
    some_function, some_other_function, and_some_other_function)

Let's take a short exercise.

Which is a better way to write multi-line strings?

# Using Backslash
aurelius_says = """Never let the future disturb you. \
You will meet it, if you have to, with the same weapons of \
reason which today arm you against the present."""

# Using Implicit line continuation
aurelius_says = (
    "Never let the future disturb you."
    "You will meet it, if you have to, with the same weapons of "
    "reason which today arm you against the present."
)
  1. Using a backslash
  2. Using implicit line continuation

We can break long lines over multiple lines by wrapping expressions in parentheses. These should be used in preference to using a backslash for line continuation.

Line Breaks

While wrapping long lines to fit inside the max 79 chars limitation, you might encounter the following scenario. Let's say we have the following code:

# Wrong:
# operators sit far away from their operands
income = (gross_wages +
          taxable_interest +
          (dividends - qualified_dividends) -
          ira_deduction -
          student_loan_interest)

For decades the recommended style was to break after binary operators. But this can hurt readability in two ways:

  • the operators tend to get scattered across different columns on the screen,
  • and each operator has moved away from its operand and onto the previous line.

Here, the eye has to do extra work to tell which items are added and subtracted. To solve this readability problem, mathematicians and their publishers follow the opposite convention.

Donald Knuth explains the traditional rule in his Computers and Typesetting series:

"Although formulas within a paragraph always break after binary operations and relations, displayed formulas always break before binary operations."

Following the tradition from mathematics usually results in more readable code:

# Correct:
# easy to match operators with operands
income = (gross_wages
          + taxable_interest
          + (dividends - qualified_dividends)
          - ira_deduction
          - student_loan_interest)

Which of the following is the recommended way to import modules ?

# All modules on the same line
import os, sys

# Separate Line for each module
import os
import sys
  1. All modules on the same line
  2. Separate Line for each module

Imports

Imports should usually be on separate lines

PEP8 guidelines state Imports should usually be on separate lines. Keeping imports on separate lines improves readability. Let's take a look in detail.

If you are importing multiple modules from a package, you can list them out in the same line.

Although if you are importing multiple modules from a package, you can list them out in the same line.

# Correct:
from subprocess import Popen, PIPE
Imports are always put at the top of the file, just after any module comments and docstrings, before module globals and constants.

And we should group them in the following order:

  1. Standard library imports.
  2. Related third party imports.
  3. Local application/library specific imports.

As we previously mentioned, wildcard imports are not recommended.

Wildcard imports (from <module> import *) should be avoided, as they make it unclear which names are present in the namespace, confusing both readers and many automated tools.

What should be the order of the imports in the following code?

from mypackage.module_name import some_function 	# 1
import os		# 2
import sys		# 3
  1. 2, 3, 1
  2. 2, 1, 3
  3. 1, 2, 3
  4. 3, 1, 2

Indentation

The Python style guide recommends using four spaces indentation level.

Use 4 spaces per indentation level.

However, there are certain cases where the indentation level should be vertically aligned to the previous level. Continuation lines should align wrapped elements either vertically using Python’s implicit line joining inside parentheses, brackets, and braces.

Let's see understand vertical alignment using an example.

# CORRECT
# Aligned with opening delimiter.
foo = long_function_name(var_one, var_two,
                         var_three, var_four)

# WRONG
# Not aligned with the opening delimiter
foo = long_function_name(var_one, var_two,
    var_three, var_four)

If you are defining a function with no arguments on the first line, then further indentation should be used to clearly distinguish itself as a continuation line.

# CORRECT:
# More indentation included to distinguish this from the rest.
def long_function_name(
        var_one, var_two, var_three,
        var_four):
    print(var_one)

# Further indentation required as indentation is not distinguishable.
def long_function_name(
    var_one, var_two, var_three,
    var_four):
    print(var_one)

When you are defining multi-line tuples, lists, then closing brace/bracket/parenthesis on multi-line constructs may either line up under the first non-white-space character of the last line of list. For instance,

my_list = [
    1, 2, 3,
    4, 5, 6,
    ]
result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
    )

Or it may be lined up under the first character of the line that starts the multi-line construct. For instance,

my_list = [
    1, 2, 3,
    4, 5, 6,
]
result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
)

What's the recommended way to write multi-line tuples?

# Style 1
result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
    )

# Style 2
result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
)
  1. Style 1
  2. Style 2
  3. Both can be used

Comments

We have earlier looked at how we should write comments. To recall, comments should focus on why the code block is required rather than what it does.

The following are a few recommendations regarding comments in PEP8.

Comments that contradict the code are worse than no comments. Always make it a priority to keep the comments up-to-date when the code changes!

Comments should be complete sentences. If a comment is a phrase or sentence, its first word should be capitalized, unless it is an identifier that begins with a lower case letter (never alter the case of identifiers!).

If a comment is short, the period at the end can be omitted. Block comments generally consist of one or more paragraphs built out of complete sentences. Each sentence should end in a period.

Apart from these general guidelines, PEP8 has some specific guidelines for block and inline comments.

Block Comments

Block comments generally apply to some (or all) code that follows them and are indented to the same level as that code.

Here is a block comment which describes the for-loop. Notice that the block comment is indented in the code-block.

for num in range(14):
	# Loop over `num` 14 times to generate the
    # multiplication table of number 7
    print(f'{num}x7 = {num*7} ')

Inline Comments

Use inline comments sparingly.

Inline comments are unnecessary and, in fact, distracting if they state the obvious.

For instance,

x = x + 1                 # Increment x

You should write why the code exists rather than what the code does.

x = x + 1				# Compensate the border

What is a good comment in place of A?

# Python Program to find the L.C.M. of two numbers
def compute_lcm(num_1, num_2):
	"Computes the L.C.M. of two numbers provided as input."

	# select the greater number
	greater = num_1 if num_1 > num_2 else num_2

	while True:
        # A
		if ((greater % num_1 == 0) and (greater % num_2 == 0)):
			lcm = greater
			break
		greater += 1

	return lcm
  1. # Break if the number is divisible by both numbers
  2. # An if statement with two conditions
  3. # Increases the greater number.
  4. # Break if L.C.M is found.

Document Strings

We earlier mentioned that documentation strings, or doc-strings, are strings enclosed in quotation marks that appear on the first line of any function or method. We can also use them while creating classes.

PEP8 makes the following recommendations while using doc-strings.

  • Write doc-strings for all public modules, functions, classes, and methods.
  • The first line should be a short description.
  • If there are more lines in the doc-string, the second line should be blank to separate the summary from the rest of the description visually.
  • The """ that ends a multiline docstring should be on a line by itself. For instance,
"""Return a foobang

Optional plotz says to frobnicate the bizbaz first.
"""			# Mark that the ending quotes are on their line
  • For one-liner doc-strings, please keep the closing """ on the same line.
  • Leave a blank space after the one line doc-string
def foo():
    """Important function"""		# One Line doc-string
    pass

What should be the doc-string at the A?

# Python program to find H.C.F of two numbers
def compute_hcf(num_1, num_2):
	"A"

	# Select the smaller number
    smaller = num_1 if num_1 < num_2 else num_2


    for i in range(1, smaller+1):
    	# If i is a divisor of both numbers, assign it as HCF
    	if((num_1 % i == 0) and (num_2 % i == 0)):
            hcf = i

    return hcf
  1. "Computes the L.C.M. of two numbers provided as input"
  2. "Computes the H.C.F. of two numbers provided as input"
  3. "Computes the smaller number of the two and checks if both input numbers are divisible by it"
  4. "Computes the H.C.F using a for-loop"

White Space

In a code structure, we can use whitespace (blank spaces) effectively to increase the code's readability. If pieces of code are cramped together, then the code becomes difficult to read. On the contrary, if there is too much whitespace, it can also be difficult to understand. Let's see what PEP8 recommends to leverage whitespace to increase code readability.

Blank Lines

Surround top-level function and class definitions with two blank lines.

Let's take a look at an example.

def top_level_function():
    return None


def an_important_top_level_function():
    return None


def another_important_top_level_function():
    return None

Mark that all the functions are separated by two blank lines.

Use blank lines in functions, sparingly, to indicate logical sections.

We can use blank lines to make the code block inside a function definition more readable. For instance, the following is a function that returns the list of prime numbers between two intervals.

def prime_numbers_between(lower, upper):
	"""Python function to return all the prime numbers
	within an interval specified by lower and upper
	"""
	not_prime = set()
	for n in range(lower, upper):
		 for x in range(2, n):
		         if n % x == 0:
		            not_prime.add(n)
		            break
	prime_numbers = set(range(lower, upper)) - not_prime
	return prime_numbers

We can rewrite the above code with some whitespace to add more readability.

def prime_numbers_between(lower, upper):
	"""Python function to return all the prime numbers
	within an interval specified by lower and upper
	"""
	not_prime = set()
	for n in range(lower, upper):
		for x in range(2, n):

			if n % x == 0:
                not_prime.add(n)
                break

    prime_numbers = set(range(lower, upper)) - not_prime

    return prime_numbers

As you can notice, adding blank spaces makes the code look cleaner. The return statement also has a separate blank line on top of it to show exactly what is being returned.

White Space in Expressions and Statements

In some cases, adding whitespace can make the code harder to read. PEP8 shows specific cases where whitespace is inappropriate.

Avoid trailing whitespace

The whitespace at the end of a line is called trailing whitespace.

The most important place to avoid whitespace is at the end of a line. It is invisible, and therefore, it can cause errors that can be difficult to trace. Other than that, the following are other examples of whitespace.

Immediately inside parentheses, brackets, or braces
# CORRECT:
spam(ham[1], {eggs: 2})

# NOT RECOMMENDED:
spam( ham[ 1 ], { eggs: 2 } )
Between a trailing comma and a following close parenthesis
# CORRECT:
foo = (0,)

# NOT RECOMMENDED:
bar = (0, )
Immediately before a comma, semicolon, or colon
# CORRECT:
if x == 4: print x, y; x, y = y, x

# NOT RECOMMENDED:
if x == 4 : print x , y ; x , y = y , x
Immediately before the open parenthesis that starts the argument list of a function call
# CORRECT:
spam(1)

# NOT RECOMMENDED:
spam (1)
Immediately before the open parenthesis that starts indexing or slicing
# CORRECT:
dct['key'] = lst[index]

# NOT RECOMMENDED:
dct ['key'] = lst [index]
More than one space around an assignment (or other) operator to align it with another
# CORRECT:
x = 1
y = 2
long_variable = 3

# NOT RECOMMENDED:
x             = 1
y             = 2
long_variable = 3
Don’t use spaces around the = sign when used to indicate a keyword argument or a default parameter value.
# CORRECT:
def complex(real, imag=0.0):
    return magic(r=real, i=imag)

# NOT RECOMMENDED:
def complex(real, imag = 0.0):
    return magic(r = real, i = imag)

What's wrong with the following code?

def FOO():
    a = input("Enter a number")
    b = input("Enter another number")
    return max( a , b )
  1. Follows the wrong naming convention for the function name
  2. No vertical whitespace
  3. Lots of whitespace in the function call of max() function
  4. All of the above

Another equally important set of guidelines that the community follows is the Zen of Python. It is a set of 19 aphorisms written by a core python member that succinctly encodes guiding principles for Python's design.

Let's look at the Zen of Python next.

Zen of Python

A literary style emerged in ancient philosophical schools of Zen-Buddhism and Taoism. In this style, subtle enlightened thoughts were not communicated directly rather encoded in self-paradoxical riddles. Because the exact intent behind the verses was not communicated, their sole purpose was to be meditated upon.

Below is an example of such verse from the Tao Te Ching.

When people see some things as beautiful,
other things become ugly.
When people see some things as good,
other things become bad.
-- Lao Tzu

The simplicity of the prose obscures the deeper hidden intent behind it. Only when someone takes some time to think about the meaning mindfully, they would understand the meaning.

What do you think the above verses from Tao Te Ching mean?

In the late 90s, programming-related queries were posted on online mailing-lists for various people to participate. Mailing lists are similar to internet forums but mostly filled with nerds. A fellow Python programmer once requested a brief document written in the Zen literary style, which explained the spirit of the python language. Let's take a look at the request first.

The following text[1] shows part of the request:

Would both Guido and Tim Peters be willing to collaborate on a short paper -- call it "The Python Way" for lack of a better title -- which sets out the 10-20 prescriptive they might offer to those who come to Python from other languages and immediately want to find a way to bend
it into uncomfortable positions?
What I have in mind is sort of a very brief Strunk-&-White-like "Elements of Style" for Python, which suggests fundamental idiomatic recommendations for operating within the spirit of the language.
A distillation of Python Zen is what I'm talking about -- something to go off and contemplate ...

Tim Peters, a core contributor of Python, took to task to jot down 20 sets of aphorisms, which was termed as the Zen of Python. The following is the text:

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!

This little piece of document, which was possibly created in a moment of playfulness, gained wide acceptance in the Python community. So much so that Python interpreter contains an easter egg that prints out Zen of Python upon importing this.

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
...

Now, these lines were not to be explained directly. As the requester rightfully points out, it is something to go off and contemplate.

Therefore, I don't have an intention to state the meaning of each aphorism explicitly. Doing so would mean revealing my ignorance. However, what I do wish to do is show you the spirit of Python using an example. You should note not taking any authoritative value of interpretation rather one of the possibilities of interpretation out of many.

Let's start looking at some instances to understand more.

Beautiful is better than ugly

The code below prints alphabets in reverse order in two ways. Which of the following two codes is more beautiful?

"""
Print alphabets list in reverse order
"""

alphabets = ['A', 'B', 'C', 'D', 'E']

# CODE - 1
for i in range(len(alphabets)-1, -1, -1): # Index-based Looping
    print(alphabets[i])

# CODE - 2
for alphabet in reversed(alphabets): # Looping over elements
    print(alphabet)
  1. Code - 1
  2. Code - 2

Although beauty is a matter of subjective interpretation, I hope you can recognize the elegance of the Code-2. You can read the code and get what it might be doing without thinking hard. Therefore, Code-2 is more beautiful and better.

Try to avoid using index-based looping.

Beautiful is better than ugly.

Explicit is better than implicit

In the following code, let's explore various ways to import and use the sqrt() function from the math module.

"""
Print the square root of number 42
"""

# Never do it
from math import *
print(sqrt(42))

# Correct
from math import sqrt
print(sqrt(42))

# Recommended
import math
print(math.sqrt(42))			# Writing alongwith module-name makes it more readable

Never import using the wildcard operator * to import all the names in a module. This might be useful for prototyping and testing. It is a terrible idea to do it on a real-world program. The reason is it implicitly imports sqrt() function from the math module.

Someone who is not familiar with the math module might not be able to discern if sqrt() is imported or is a built-in function. Implicitly importing is a bad idea.

The two other import methods, such as from math import sqrt and import math, the second one is often recommended in many instances as it leads to readable code. However, when you import from nested sub-packages or longer module names, the first one makes more sense.

This aphorism essentially encourages writing code explicitly with-holding the urge to use obscure language features.

# CODE - 1
def guest_list(*args):
    guest_1, guest_2 = args
    return dict(**locals())


# CODE - 2
def guest_list(guest_1, guest_2):
    return {'guest_1': guest_1, 'guest_2': y}
  1. Code - 1
  2. Code - 2

In the second code above, guest_1 and guest_2 are explicitly retrieved in the function definition, and an explicit dictionary is returned. The programmer using the function knows exactly what to do by reading the first and last lines, which is not the case with the first case.

Explicit is better than implicit.

Simple is better than complex. Complex is better than complicated.

Some of the problems are simple and straightforward. Some are complex. Similarly, your solution can be simple or complex. For instance, some problems are specifically designed to be solved by list comprehensions. Solving it using list comprehensions makes a simple solution.

# From 1 to 100, return a list of numbers that are even multiples of 7 while subtract by two

def even_multiples(): # Complex Solution
    multiples = []						# Assignment
    for num in range(1, 100):			# loop
        if num % 14 == 0: 				# Conditional
            multiples.append(num - 2) 	# Assignment and Update
	return multiples


def even_multiples_2():	# Simple Solution
    return [num - 2 for num in range(1, 100) if num % 14 == 0 ]

You might say the first-way writing is also simple and straightforward and for those you are not aware of the list comprehensions, this is quite true. However, compared to list comprehensions, the first solution uses multiple statements. It is relatively less easy to decipher than list comprehensions, given you get familiar with the list comprehensions.

But for many cases, list comprehension becomes a bad idea. Let's say you wish to write a FizzBuzz[1:1] program.

"""
Write a function `FizzBuzz` program.
"""

def fizzBuzz():
    result = []
    for x in range(1, 100):
        if x % 15 == 0:
            result.append("FizzBuzz")
        elif x % 5 == 0:
            result.append("Buzz")
        elif x % 3 == 0:
            result.append("Fizz")
        else:
            result.append(x)

	return result


def fizzBuzz_2():
    return ["FizzBuzz" if x % 15 == 0
           else "Fizz" if x % 3 == 0
           else "Buzz" if x % 5 == 0
           else x
           for x in range(1, 100)]

In this case, list comprehensions become a complicated solution using multiple nested if-else ternary operators. The fizzBuzz_2 function is compact; however, using so many if-else renders itself unreadable at this point.

The following is the thumb rule for determining the simplicity of programs:

  • simple solutions are easier to read and understand,
  • complex involves statements which could have been avoidable
  • complicated are not easier to read and understand and include statements that could have been avoidable.

Do keep in mind that simple and complex are relative terms.

Which of the following is a simple solution?

# Sum of natural numbers up to positive integer `num`
# Code - 1
def sum_upto(num):
    if num < 0:
   		print("Enter a natural integer")
	else:
        sum = 0
		# use while loop to iterate until zero
		while(num > 0):
			sum += num
			num -= 1

# Code - 2
def sum_upto(num):
    return sum(range(num+1))
  1. Code - 1
  2. Code - 2

The second code uses the sum() function to express in simpler terms. Therefore, it is a simple solution.

Simple is better than complex. Complex is better than complicated.

Flat is better than Nested

Let's write a program that classifies if a given organism in a photo. We won't be writing the full code rather just a pseudo-code to understand how writing such a classifier will look.

"""
Write a function that classifies animals
"""
def animal_classifier(animal):
    if has_backbone(animal):						# Vertebrate
		if blood(animal) == 'Warm': 				# Warm-blooded Animals
            if has_feathers(animal):				# Skin is feathers
                return 'Bird'
            else:									# Skin is hairy or fur
                return 'Mammal'
        else:										# Cold-blooded Animals
            if not survive_on_land(animal):
                return 'Fish'
			else:
                if has_moist_skin(animal):
                    return 'Amphibians'
                else:
                    return 'Reptiles'
    else:											# Invetebrates
        if has_legs(animal):
            return 'Anthropod'
        else:
            return 'Worms'

The above animal classifier is has a lot of nested if-else statements. We can rewrite the above code to have a less nested structure in the following way.

"""
Write a function that classifies animals
"""
def animal_classifier(animal):
    if has_backbone(animal):
		return detect_vetebrate(animal)
    else:
    	detect_invetebrate(animal)

def detect_vetebrate(animal):
    if blood(animal) == 'Warm':
        return 'Bird' if has_feathers(animal) else 'Mammal'
    else:
        return detect_cold_blood(animal)

def detect_cold_blood(animal):
    if not survive_on_land(animal):
        return 'Fish'
    elif has_moist_skin(animal):
        return 'Amphibian'
    else:
        return 'Reptile'

def detect_invetebrate(animal):
    return 'Anthropod' has_legs(animal) 'Worm'

The above code achieves the same thing, but it is more concise, compact, and more readable.

Flat is better than nested.
Would you like to describe the meaning of aphorism from the above example?

Sparse is better than dense

We had earlier written a prime number generator. Let's write another one to understand our next aphorism.

Our previous generator function was the following:

def generate_prime(start = 3):
	if start < 3 :
        raise ValueError("Number cannot be less than 3")
    elif type(start) == float:
        raise ValueError("Number cannot be float")
    num  = start
	while True:
		if not has_divisors(num):
			yield num
		num += 1

def has_divisors(num):
	for x in range(2, num):
		if num % x == 0:
			return True
        return False

We can rewrite the above by separating them into few lines as prescribed by the PEP8 recommendation we saw earlier.

def generate_prime(start = 3):
	num  = start
    if start < 3 :
        raise ValueError("Number cannot be less than 3")

    elif type(start) == float:
        raise ValueError("Number cannot be float")

    while True:
		if not has_divisors(num):
			yield num
        num += 1


def has_divisors(num):
	for x in range(2, num):

        if num % x == 0:
			return True

        return False

Adding few blanks as whitespace in the code makes it easier to read. Do not try to fit more code in one line or cramp together a bunch of lines.

Sparse is better than dense.

Why don't you interpret the aphorism in your own words?

Readabiility Counts

Often you need to write functions that accept several parameters. You might be tempted to use positional arguments instead of named arguments to write faster. This leads to poor readability of the code.

We can illustrate this using the following functions. The create_avatar() function can be invoked in the following way.

>>> create_avatar("Luffy","Black", "Red", "Golden",)
>>> create_avatar("Zorro", "Green", "White")
>>> create_avatar("Sanji", "Golden", "Black")

In the above code, we can determine that the create_avatar() takes three or four arguments, although what each of the arguments corresponds to is not clear. Now, let's invoke the function with named arguments.

>>> create_avatar(name="Robin",
                  hair_color="Black",
                  shirt_color="Black",
                  hat_color="Black")

Don't you think invoking with named arguments is much more readable?

Your code will find it easier for someone to read your code when invoking your custom functions with explicitly named arguments. It might be faster using just the positional arguments. Still, it is peanuts compared to the collective amount of time saved in the long run by using named arguments. A good readable code saves time.

Readability counts.

Let's say you write the following function:

def design_shirt(size, color, brand, hoodie=False):
    # Shirt Designing Code

Which of the following is the preferred way of invoking the function?

# Code - 1
>>> design_shirt(size=32, color="black", brand="Juju", hoodie=True)

# Code - 2
>>> design_shirt(32, "black", "Juju", True)
  1. Code -1
  2. Code -2

Special cases aren't special enough to break the rules.

Our next aphorism might seem a bit contradictory. We can interpret it because their rules, guidelines, and conventions are there to boost the community's efforts to enforce consistency. As R.W.Emerson noted:

However, a foolish consistency is the hobgoblin of little minds.

There are times when the guidelines and conventions are not applicable. PEP 8 marks the following cases where it is a good reason to ignore the guidelines.

  1. Applying the guideline would make the code less readable, even for someone used to reading code that follows this PEP.
  2. To be consistent with surrounding code that also breaks it (maybe for historic reasons) -- although this is also an opportunity to clean up someone else's mess (in true XP style).
  3. Because the code in question predates the introduction of the guideline, and there is no other reason to be modifying that code.
  4. When the code needs to remain compatible with older versions of Python that don't support the style guide's feature.

Practicality beats purity.

What do you think the above aphorism means?

The next aphorism relates to error handling we covered earlier.

Errors should never pass silently. Unless explicitly silenced

Let's take a look at what it means. Take the following code.

for num in range(-2, 2):
        print(10/num)

The above code will result in raising ZeroDivisionError when num becomes 0. We can catch the exception in a try-except clause.

for num in range(-2, 2):
    try:
        print(10/num)
    except:
        pass					# Silencing the errors

Although the above code avoids raising an error, the except block captures every error in Python and silently passes it. This is a pretty bad practice.

You should explicitly name the exceptions you are prepared to recover from and only catch those. In this case, the better approach is the following code.

for num in range(-2, 2):
    try:
        print(10/num)
    except ZeroDivisionError:
        print("Division by Zero is not allowed")

Completely avoid using pass in except blocks and always specify which exceptions you expect to deal with. Errors should never pass silently.

Whenever you find an error, you should do something to handle it, such as logging it in a txt file, etc. At least, it informs you that there used to be an error. In general, you should explicitly define exceptions you want to catch to avoid catching unwanted exceptions.

You should know what exceptions you ignore.

Errors should never pass silently. Unless explicitly silenced.
What do you think will happen if you let all errors pass silently?

If you let all errors pass silently, you might not be able to correct debug when your program fails due to an unanticipated error. Sometimes, you might not even be aware your program is failing. Therefore it is a really bad idea. Let's move on to the next aphorism.

Refuse the temptation to guess

While building programs, programmers often make a lot of guesses or have assumptions. Let's take the following code example.

""" Find the square root of the user-provided number """

import math

def find_squareroot():
    number = int(input("Enter the number to find the square-root: "))
    result = "Square-root for {} is {:.2f}"
    			.format(number, math.sqrt(number))

    print(result)

The above has the underlying assumption that the user will always provide inputs that are integers. If the user enters non-integral characters, the program will raise crash and raise an exception. We can easily bypass our assumption by ensuring that the program can handle cases when the user provides invalid input.

from math import sqrt

def find_squareroot():
    try:
        number = int(input("Enter the number to find the square-root: "))

    except ValueError:
        print("Please enter valid integer")
        find_squareroot()

    else:
        result = "Square-root for {} is {:.2f}"
        			.format(number, sqrt(number))
        print(result)

In the face of ambiguity, refuse the temptation to guess.

What's your interpretation of the above aphorism?

If the implementation is easy to explain, it may be a good idea

Python interpreter contains an easter egg which prints out the Zen of Python upon importing this. Python has a handy standard module inspect having a getsource() function, which lets you see a given module's source code.

Let's take a look at the source code of the this module.

The following shows the source code of the this module.

>>> import inspect, this
...			# output of zen of python from importing `this`
>>> print(inspect.getsource(this))
s = """Gur Mra bs Clguba, ol Gvz Crgref

Ornhgvshy vf orggre guna htyl.
Rkcyvpvg vf orggre guna vzcyvpvg.
Fvzcyr vf orggre guna pbzcyrk.
Pbzcyrk vf orggre guna pbzcyvpngrq.
Syng vf orggre guna arfgrq.
Fcnefr vf orggre guna qrafr.
Ernqnovyvgl pbhagf.
Fcrpvny pnfrf nera'g fcrpvny rabhtu gb oernx gur ehyrf.
Nygubhtu cenpgvpnyvgl orngf chevgl.
Reebef fubhyq arire cnff fvyragyl.
Hayrff rkcyvpvgyl fvyraprq.
Va gur snpr bs nzovthvgl, ershfr gur grzcgngvba gb thrff.
Gurer fubhyq or bar-- naq cersrenoyl bayl bar --boivbhf jnl gb qb vg.
Nygubhtu gung jnl znl abg or boivbhf ng svefg hayrff lbh'er Qhgpu.
Abj vf orggre guna arire.
Nygubhtu arire vf bsgra orggre guna *evtug* abj.
Vs gur vzcyrzragngvba vf uneq gb rkcynva, vg'f n onq vqrn.
Vs gur vzcyrzragngvba vf rnfl gb rkcynva, vg znl or n tbbq vqrn.
Anzrfcnprf ner bar ubaxvat terng vqrn -- yrg'f qb zber bs gubfr!"""

d = {}
for c in (65, 97):
    for i in range(26):
        d[chr(i+c)] = chr((i+13) % 26 + c)

print("".join([d.get(c, c) for c in s]))

The implementation of the this module is shown above. You might find it difficult to believe, but this is the same text you see when importing this. The name s in the source code stores the text of the zen of python encrypted in ROT-13 or (Rotate by 13 places ) cipher. To get the underlying text, you have to substitute each character with the 13th letter after in the alphabet.

The following shows the cipher key.

a <-> n
b <-> o
c <-> p
...
m <-> z

A <-> N
B <-> O
C <-> P
...
M <-> Z

Therefore, the nested for loops in the source code decipher the ciphered text and print out the result.

Surely, this seems to be an overkill for something which could have been much more straightforward. But it will make sense if you realize that Tim Peters penned down the Zen of Python with a sense of humor. Because the text captured the essence of programming in Python briefly, it was adopted by the Python community.

Therefore it makes sense that the source code of the this module is written in this incomprehensible way, defying all the points and as opposed to all the principles Zen of Python itself seems to be purporting. This is a case of programming humor.

However, the take away is pretty straight forward.

If the implementation is hard to explain, it's a bad idea. (unless it is a joke, of course)
If the implementation is easy to explain, it may be a good idea.

So far, we have seen both the PEP8 guidelines as well as the Zen of Python. You should take them as guiding principles to write better code in Python. In the next and final topic of the book, we will look at some additional case studies to understand Python's way.

The way of Python

A programming idiom is a way to write code to accomplish a task.

There are many ways to accomplish the same task in Python, but there is usually one preferred way to do it. This preferred way is called idiomatic Python or Pythonic.

Over the years, the Python community has identified some of the best practices of using Python.

What is an example of best practice in Python that comes to your mind?

An example would be following the PEP8 convention of writing constants in all caps, such as MAX_OVERFLOW. This makes it easier for others to recognize that an identifier is a constant. Let's look at some additional features of Pythonic code.

When an experienced Python programmer complains of code not being Pythonic, they usually mean either of the following:

  • the lines of code don't follow the common guidelines (as specified by PEP8 or similar document)
  • the lines of code fails to express what it is trying to perform in a more readable way
  • the lines of code fail to take advantage of a feature of Python, which is designed for the same operation.

The Zen of Python states[1:2],

There should be one-- and preferably only one --obvious way to do it

The way to write Pythonic code is not obvious for programmers starting with Python, therefore, as beginners, you must acquire them. Let's look at some of these idioms to get an idea about how Python works. We will start with the first case of the swapping of two values referenced by two identifiers.

Swapping two values

We have following two objects, which store countries and their respective capitals in two lists. Unfortunately, we have stored the capitals in countries and countries in capitals.

countries = ["New Delhi", "Washington D.C.", "London"]
capitals = ["India", "USA", "UK"]

We need to swap the values of country with capitals and vice versa.

What will be your approach to solve the problem?

If you come from a different programming background, you might be tempted to use a temporary name to achieve the task.

For example, storing the value of capitals in a temporary name temp and then assigning the value of countries to capitals and finally assigning the temporary name temp to capitals.

The following shows how two values can be swapped using a temporary name.

# Not Pythonic
temp = capitals
capitals = countries
countries = temp

Although this will give you the desired result, this is not the way of Python. Whenever you wish to swap values between two variables, you should use a tuple to make your intention clearer.

# Pythonic
(countries, capitals) = (capitals, countries)

You can also do the same for three or more objects. Suppose we have the following names which need to be swapped.

country = ["New Delhi", "Washington D.C.", "London"]
capitals = ["Mahatma Gandhi", "Hollywood", "Colonization"]
major_language =  ["India", "USA", "UK"]
famous_for = ["Hindi", "English(US)", "English(UK)"]

We can swap the variables as follows:

(country, capitals, major_language, famous_for) = (major_language,
                                                   country,
                                                   famous_for,
                                                   capitals)

What is the final value of w?

>>> w, x, y, z = -2, 1, 10, 5
>>> (y, z, x, w)  = (z, x, y, z+x-2*w)
>>> w

What is the final value of w?

  1. -2
  2. 1
  3. 10
  4. 5

Using tuples conveys what we wish to achieve more expressively. When you write code in a way that makes your intention clear, it is easier to maintain. Now, let's look at another idiom.

Unpacking values

Sometimes, you want to store values from a sequence in a name for easier access later on.

>>> person = ["Harry", "Potter", "Wizard", None, None, None, "Hogwarts"]

You might be tempted to use indexing to store items from the list.

# Not Pythonic
>>> first_name = person[0]
>>> last_name = person[1]
>>> occupation = person[2]
>>> school = person[6]

However, in Python, it is recommended to use unpacking to get individual elements.

# Pythonic
>>> (first_name, last_name, occupation, *_ , school ) = person

You can use the (*) operator along with underscore (_) to ignore the values. Although you can use any other name instead of _, it is a convention to use _ to imply discarded values.

What's the value of the phone, occupation, _ in the following code listing?

>>> person = ["Harry", None, "Potter", None, "Wizard", None, None, None, "Hogwarts"]
>>> (first_name, middle_name, last_name, phone, occupation, birth_place, *_, school_name) = person
  1. None, "Wizard", [None, None, None]
  2. "Potter", None, None
  3. "Wizard", None, "Hogwarts"
  4. None, "Wizard", [None, None]

In Python, readability and expressiveness are given utmost importance. As we saw earlier, a general convention of Python is to write one statement per line so that it's easier to read and understand later on. Let's look at it in detail.

One Statement per line

You might be tempted to cramp two short sentences in the same line. It's not the way of Python.

# Not Pythonic
age = current_year - birth_year ; print(f'He is approx {age} years old')

# Pythonic
age = current_year - birth_year
print(f'He is approx {age} years old')

This is also true for if statements with a shortcode block. You might be tempted to put the condition and the code block on the same line, but it is a bad practice and not recommended.

# Not Pythonic
if age > 18: print("You are allowed to enter")

# Pythonic
if age > 18:
    print("You are allowed to enter")

We often need to test the truth value of an identifier in the if condition. For instance,

# Not Pythonic
if something == True:
    # do something

if something_else == ():
    # do something

if other_thing == None:
    # do something

In Python, we don't need to explicitly compare a value to True, None, 0, or empty collections. We can add it to the condition and let Python determines its Boolean value. Recall that None, zeroes in any numeric types, and empty sequences and collections have the truth value False. So, we can write the following code instead.

# Pythonic
if something:
    # do something

if not something_else:
    # do something

if not other_thing:
    # do something
What do you think the expression one statement per line means?

One way to think about it is the following.

Each line should ideally express a single idea.

We can understand this idea better using another example.

Let's take another instance. Let's suppose we have an if statement with compound conditions that might continue to the next line.

The following code listing demonstrates the same.

# Not Pythonic
if person["age"] > 18 and person["age"] < 25 person["height_in_cms"] > 170 and person["weight_in_kgs"] > 70 and person["weight_in_kgs"] < 95:
   # do something

In this case, we can break the conditions, store them in an identifier, and refer them in the if statement separately.

The following code listing demonstrates the same.

# Pythonic
age_condition = person["age"] > 18 and person["age"] < 25
height_condition = person["height_in_cms"] > 170
weight_condition = person["weight_in_kgs"] > 70 and person["weight_in_kgs"] < 95

if age_condition and height_condition and weight_condition:
    # do something

Avoid extended statements.

Another common use-case is to check a name against several values in an if condition.

# Not Pythonic
if name == "LUFFY" or name == "ZORRO" or name == "SANJI":
    # do something

Repeating the name can make the statement unnecessarily verbose. We should use a temporary collection to express the intention more clearly.

# Pythonic
if name in ('LUFFY', "ZORRO", "SANJI"):
    # do something
Avoid verbosity

Are the two codes below equivalent?

guests = ["Nami", "Robin", "Boa", "Rebecca"]
undesired_guests = ("Luffy", "Sanji", "Zorro")

# Code - 1
undesired_absent =  all(guest not in undesired_guests for guest in guests)
if undesired_absent:
    print("Ladies only")

# Code - 2
for guest in guests:
    if guest in undesired_guests:
        break
else:
    print("Ladies only")

  1. Yes
  2. No

The first one uses a generator expression inside the built-in all(). In contrast, the second one uses the else statement and the for statement. Both Code-1 and Code-2, in the above exercise, are equivalent.

Let's look at another popular Python idiom: creating a list of n-length objects.

Often, we need to create an n-length list of the same object. We can write things directly, but it's a bit cumbersome.

# Not Pythonic
bunch_of_ones = [1, 1, 1, 1, 1, 1, 1, 1, 1]

We can also use a for loop.

# Not Pythonic
bunch_of_ones = []
for i in range(8):
    bunch_of_ones.append(1)

Or even list comprehension to do the same.

## Not Pythonic
bunch_of_ones = [1 for num in range(8)]

Rather than typing it or using loops or using comprehensions, we can easily do so using the * operator.

# Pythonic
bunch_of_ones = [1]*9

However, when we want to create an n-length list of empty list object, using the * operator is not a good idea. It would create N references to the same list object.

>>> bunch_of_lists = [[]]*5
>>> bunch_of_lists[1].append(1)
>>> bunch_of_lists
[[1], [1], [1], [1], [1]]

Unless you want copies of the same object, we should use the list comprehension to generate a list of an n-length empty list object.

# Not Pythonic
bunch_of_lists = [[]]*5

# Pythonic
bunch_of_lists = [[] for L in range(4)]

Time for a short exercise.

What's the output of the below code?

>>> [1, 2, []*2]* 2
  1. [1, 2, [], 1, 2, []]
  2. [2, 4, [], []]
  3. [1, 2, [], [], 1, 2, [], []]
  4. [2, 4, [], [], [], []

A common pattern while accessing a dictionary element is to check if it exists and then tries to access it. A dictionary object has a has_key() method to check if a key exists. Although a better way exists to access a dictionary element, let's take a look.

Accessing a dictionary key

If we are not aware of what keys are presented in a given dictionary, we might end up raising KeyError.

>>> person = {"name" : "Luffy"}
>>> person["age"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'age'

We can use the has_key() method of dictionary objects to check if a key exists and then try to retrieve it, although this is not Pythonic.

person = {"name" : "Luffy"}

# Not Pythonic
if person.has_key('age'):
    print(f"His age is {person['age']} ")
else:
    print(f"His age is not known")

Python dictionary objects have a get() method, which you can use to check for keys instead.

# Pythonic
person.get('name', 'Name not known')	# Returns `Luffy`
person.get('age', 'Age not known')		# Returns `Age not known`

You can also use the in operator to check if a given key exists inside a dictionary.

if 'age' in person:
    print(f"His age is {person['age']} ")

Let's take another short exercise.

Will the following code execute?

person = {"name" : "Luffy"}

if 'Luffy' in person.values():
    print("Hurray")
else:
    print(':(')
  1. Hurray.
  2. :(

Let's move on to the next idiom. If you work on large sets of data, generator expression often is much faster than list comprehensions. Let's take an example to understand more.

Let's say you wish to find the minimum of a particular attribute of an object amidst a huge list of objects—for instance, countries and population size.

>>> countries = [{"name": "India", "population": 1.3e9 }, ...]

To find the minimum, you can use the built-in min() function by using list comprehension.

# Not Pythonic
lowest_population_country = min([(country.population, country.name)
                             for country in countries])

However, a better way would be to use generator expression instead.

# Pythonic
lowest_population_country = min(((country.population, country.name)
                             for country in countries))
Can you think of reasons why generator expressions are preferred over list comprehensions?

Both list comprehensions and generator expressions follow almost the same syntax. However, a list comprehension creates a new list that requires more memory while a generator returns an iterator. Next, let's look at the last idiom we will cover: Reading from a file.

Reading from a file

As we mentioned in the last chapter, there are two ways to open a file.

You can use the try-finally clause to read the file and ensuring that it is automatically closed.

# NOT RECOMMENDED
try:
    file_reader= open('some_text.txt')
    # do something
finally:
    file_reader.close()

You can also use the with open statement to automatically close the files for you after reading. Using the with open to read files is recommended over manually closing files.

# Pythonic
with open('some_text.txt') as file_reader:
    # do something

And this brings us to the end of this chapter as well as this course.

Before finishing up, let me ask you a question. Can you recall which protocol allows the with statement to close the file after reading automatically?

The answer is context manager protocol. We didn't cover the context manager protocol in detail in this course. We also left out the many important topics such as classes, object-oriented programming, networking, etc., in Python. We will cover them in the next courses on Python.

In this course, we have managed to pack some very interesting features of Python.

I don't expect you to understand or remember 100 % of it by now. That would be crazy. No one learns a subject simply by completing a course on it once.

Therefore, before parting, I would like to make some suggestions to strengthen your course learning.

  • Return to this book and review it once in a while.
  • When you do end up reviewing, try to write concepts in your own words.
  • Come up with new questions and seek its answer.
  • Consult the Zen of Python often.
  • Read good Python code.
  • Write code often and a lot.

And most importantly, enjoy the adventure. I hope you had a good time learning Python. You can always contact me at [email protected]. I would love to hear about your experience.

And with this, see you next in the course.


https://mail.python.org/pipermail/python-list/1999-June/001951.html Python Mail Archive ↩︎ ↩︎ ↩︎