The Zen of Python

As a segue from beginner to intermediate material, I want to share something with you and use it to generate some examples.


In [ ]:
import this

What's this? I guess you could call it an "Easter Egg", a surprise put into the standard library. But it's also a set of rules and conventions that Python coders try to follow. Let's explore them.

Beautiful is better than ugly.

Here is a good time to point out PEP8. PEP stands for Python Enhancement Proposals and it is where ideas for changing Python are formalized, to eventually become part of the langauge or die out. If you browse the PEPs you can read about the rationale for many language features at the time they were first proposed, and see some potential new features (PEP465 is exciting for scientists!). Anyways, PEP8 is a little different, it contains all the style recommendations for coding in Python, such as where to put whitespace and how to name things. Sticking to the convention makes it easier for others to read your code (and trains your own eyes to better read others' code). At least skim it.

For example, make your varaible and function names descriptive (there is never a good reason to call a variable temp), and use lowercase_with_underscores.

(Another different sort of PEP is PEP20...)

Also code can be beautiful (see Euclidean algorithm for greatest common divisor):


In [ ]:
def gcd(a, b):
    while b != 0:
        a, b = b, a % b
    return a

In [ ]:
gcd(120, 32)

Your code shouldn't be too beautiful that it runs foul of the next one though...

Explicit is better than implicit

Well, here's a good reason to avoid from module_name import *. It's not explicit!

Another example is (part of) the rationale for changes like this in Python 3:


In [ ]:
some_dict = {'a': 2, 'b': 3}
list(some_dict.items())

Well, there was a much stronger rationale for not returning a list by default--we'll see it later, but being explicit is a reason that it wasn't considered a drawback to have to use list when you want a list.

Simple is better than complex

Complex is better than complicated

Flat is better than nested


In [ ]:
def nested(x, y):
    if x > 0:
        if y > 100:
            raise ValueError('y is too large')
        else:
            return y
    else:
        if x == 0:
            return False
        else:
            raise ValueError('x cannot be negative')

In [ ]:
def flat(x, y):
    if x > 0 and y > 100:
        raise ValueError('y is too large.')
        
    elif x > 0:
        return y
    
    elif x == 0:
        return False
    
    else:
        raise ValueError('x cannot be negative')

Sparse is better than dense


In [ ]:
from math import sqrt

def fancy_sqrt(x):
    if x > 0: return sqrt(x)
    elif x == 0: return 0
    else: return 1j * sqrt(-x)

In [ ]:
def fancy_sqrt(x):
    if x > 0:
        return sqrt(x)
    
    elif x == 0:
        return 0
    
    else:
        return 1j * sqrt(-x)

Note: not actaully necessary; math.sqrt handles negative numbers fine.

Readability counts

See above.

Also a reason that Python uses and and or instead of && and ||.

In short, remember the principle that most of your code will be read more often by people (e.g. you) than computers.

Special cases aren't special enough to break the rules

This is why Python has no special type for characters (a special case of strings of length 1).

Also, don't write functions that take either a list of elements or a single element. The single element is a special case; it's more consistent and not that much of a hassle to require the function caller to use a single-element list.

Although practicality beats purity

Sometimes you do have to break the rules ...

Errors should never pass silently


In [ ]:
# Don't do this:
try:
    import yaml
except ImportError:
    print('yaml module not available')

Why? Because if there's something wrong with the yaml module, and it raises an ImportError, you are going to be very confused trying to figure out why it's saying yaml is not available when you know you installed it.

Unless explicitly silenced

In the face of ambiguity, refuse the temptation to guess


In [ ]:
1 + '1'

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


In [ ]:
some_dict = {'a': 1, 'b': 2}

try:
    c = some_dict['c']
except KeyError:
    c = 0
    
# instead we have the get method
c = some_dict.get('c', 0)

In [ ]:
names = [
    'Alice',
    'Bob',
    'Carol',
]

all_names = ''
for name in names:
    all_names += name
    if name is not names[-1]:
        all_names += ', '
print(all_names)
        
# instead we have the join method
print(', '.join(names))

Although that way may not be obvious at first unless you’re Dutch

Reference to Guido Van Rossum, BDFL (Benevolent Dictator For Life)

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!

A namespace is a mapping of names to values.

  • Dictionaries are namespaces.
  • File systems are nested namespaces.
  • The Python builtins plus all the variables and functions you've defined is a namespace.
  • What you can type after the . of an object (e.g. methods) is a namespace.
  • Imported modules are namespaces.

Having lots of namespaces is nice because it reduces the chance of collisions. It lets us choose good names!


In [ ]:
def chase():
    from animals import cat, dog, mouse
    
    dog.chase(cat)
    cat.chase(mouse)

animals is not a real module, but if it were, the dog.chase and cat.chase would be two completely different things because they're in different namespaces. However we can probably assume some symmetry if the module is well-designed.