kuda.ai | code. guitar. life.

Clean Code Python PEPs

created in January 2023

When Python was released in the 1990s, it was way different to today's version. Python has, obviously, continously improved over time. From the 2000s, the Python team and community have published "Python Enhancement Proposals" (in short: "PEP").

Today I want to present a few of them. By following them, your code will instantly become better: Easier to read and understand (for your future self and others), and easier to maintain.

PEP20: The Zen of Python

Although this is not exactly to follow literally, it expresses what “Pythonic Thinking” is. It’s somewhat amazing that this is referred to as an “Easter Egg”, since in just 20 lines, it conveys so much of what makes Python so accessible.

Open your python, and type import this — here is what you will get:

>>> 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.
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!

PEP8: Standard Python Style Guide

Although I nowadays use the black code formatter, I learned a lot of “Pythonic” code style from PEP8. And I instantly recognize when someone coded without being aware of PEP8.

Some of the guidelines I encountered disregarded are for instance the order of imports or the line length.

When you list your imports at the top, PEP8 wants you to start with the builtin packages (e.g. os or json), then with downloaded packages (e.g. pandas as pd or fastapi) and thirdly and finally, with your own code that you import (e.g. from db_client import DBClient).

# incorrect according to PEP8
import pandas as pd
import os
from db_conn import DBConn
import json

# correct according to PEP8
import json
import os

import pandas as pd

from db_conn import DBConn

Second line of order is ordering by the names, alphabetically, e.g. json before os.

There is a tool that sorts your imports automatically: isort. Just run isort filename.py or isort src/ and it will update all imports in your files. You could run isort before each commit. You could even setup precommit hooks that will check if your sorts are ordered correctly before accepting a commit.

When it comes to line length, PEP8 advises 79 characters. This is less than most people use, and less than black default line length of 88.

By limiting the number of things on a line, you will naturally spread these things over multiple lines.

In the following example, both function definitions are valid, correct python. The first examples has lots of things in one line, the second example spreads them to multiple lines.

# keep many things in one line
# This makes the code hard to read
def encrypt(message: bytes, public_key: RSAPublicKey):
    ciphertext = public_key.encrypt(message, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None))
    return ciphertext

# spread many things to multiple lines
# This is way easier to read and understand
def encrypt(message: bytes, public_key: RSAPublicKey):
    ciphertext = public_key.encrypt(
        message,
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None
        )
    )
    return ciphertext

PEP484: Type Hints

Python’s dynamic typing system may make reading and understanding Python code obscure and difficult. If a function takes file as input, is it a string, or a file_descriptor? And what does your function return, a dictionary, a list of dictionaries?

Type hints to the rescue, they will remove all ambiguity!

  • Close each parameter with a comma, and append the type to it.
  • If it accepts multiple types, separate them with the pipe symbol, e.g. str | int
  • If it requires a list of strings, define them as list[str]
  • Also annotate the type your function is going to return.
def answer(q: str) -> int:
    return 42

def rainbow() -> None:
    print("🌈")

Generally, type hints have no impact on your code’s performance (but it could if you type hint everything and use certain packages, but at that point, if you need the performance, you would probably pick a strongly typed, compiled language).

A great benefit is that your code editor will make methods available. Two examples:

If you annotate your arg as type str, you will see all available methods in your IDE if you type a dot after it, see here:

pep484_1

if you do not give type annotations, your IDE is not going to help you too much.

pep484_2 if you declare the parameter s to be of type str, your IDE will start give you tips on what you can do with the parameter. Nice, innit?

pep484_3 if your functions take objects or types / classes, you won’t get any suggestions for their methods.

pep484_4 you can just pass the class as type hint. That way, people that use your code will know what to pass as argument, and their IDE will help them to see what methods are available.

PEP257: Docstrings

After you declare a function, wrap the docstring inside three double quotes on each side.

def answer(question: str) -> int:
    """Give the answer to everything."""
    return 42

Summarise what it does in one sentence. Close it with a dot.

If your functions needs more explanation, explain it in multiple lines. Explain what your function does, what argument it takes, what it returns, and what exceptions it could raise. (I learned this from Google Python style guide).

Although my example is a bit exaggerated, I think it conveys the point:

def get_json_file_paths(path: str, filename: str = "*.json") -> str:
    """Walk path and yield all file paths that match the filename.

    Args:
        path (str):
            The function will start from this directory and walk 
            all nested directories.

        filename (str, optional):
            Whenever a file matches this string, the function will yield
            the absolute path to this file. Pass for instance "*.json" to
            yield all files that have the .json extension. 

    Yields:
        A string that is the absolute path to the file that was found.

    https://docs.python.org/3/library/os.html#os.walk
    https://docs.python.org/3/library/glob.html#glob.glob
    """
    for dirpath, dirnames, filenames in os.walk(path):
        files_paths = glob.glob(os.path.join(dirpath, filename))
        for file_path in file_paths:
            yield os.path.abspath(file_path)

This way, whenever you are using a symbol (function, class), your IDE will show you your docstrings and the type hints. See this example from os.getenv:

getenv_docstrings

Conclusion

You have learned how your Python code will become cleaner by embracing the "Zen of Python", by getting familiar with PEP8 or black, and by using type hints and docstrings.

Bonus content if you made this far (thank you!): I can recommend listening to a podcast episode from RealPython where they talked with Lukasz Langa, a core Python team member and the creator of Black: Real Python Podcast #7: AsyncIO + Music, Origins of Black, and Managing Python Releases

I hope you enjoyed it, happy coding!