A Sensible, Radical Python Style Guide

I’ve read a lot of unreadable Python. Not more than some, but probably more than most. Most Python is the equivalent of this Berklee professor jam. It’s like everybody’s doing their own thing without listening to anybody else, nor caring what anybody else has to say, nor caring about the user experience whether the user is a programmer or not.

I also think that, in many cases, documentation is a needless abstraction when non-clever method/function names, variable names, parameter lists, and other basic Python features are more than enough to get the job done. Python gets a needlessly bad rap for being unreadable, partially because it has features that are often meant for hobby scripts or personal usage, like the entire global namespace. I love writing sloppy Python for scripting, but can’t stand it for actual programming.

What follows is a simple manifesto to dictate how I want to write this lang from now on:

I. Anonymous Functions

This idea comes from the Google C++ style guide, probably this style guide’s only legitimate contribution to programming. I have two examples, one good, and one bad:

class c:
    def __init__(self, n):
        # m is the reverse string of n
        self.m = ""
        self.reverse(n)

    def reverse(self, n):
        m = []
        for i in range(len(n), 0, -1):
            m.append(i)
        self.m = m

This is a class that intends to construct a reverse string from an input. The general idea behind this illustration is that, in many classes, the user must supply some information to the constructor that the class then manipulates for its own purposes. But, notice how this already unreadable code can be readily, utterly destroyed with a few innocent but catastrophic mistakes:

class c:
    def __init__(self, n):
        # m is the reverse string of n
        self.m = self.reverse(n)

    def reverse(self, n):
        m = []
        for i in range(len(n), 0, -1):
            m.append(i)
        return m 

Already we’ve made some changes that are somewhat innocent on the surface, but see how I can make new members of my class outside of the constructor? This is already a recipe for unreadability. Now, consider what I think is the better implementation:

class d:
    def __init__(self, n):
        # m is the reverse string of n
        self.n = self.reverse_string(n)

    def reverse_string(self):
        return anon_reverse_str(self.n)

def anon_reverse_str(str):
    out = ""
    for i in range(len(str), 0, -1):
        out = out + str(i)
    return out

Here I’ve got a few more lines than before, but notice how this code is, essentially, self-documenting. If I jump to the definition of this class, there’s a few things to note here:

  1. My function reverse_str function has details that are outside of the class. In large class definitions, this is perfectly fine! because I can ignore the implementation details unless they are broken or inefficient.
  2. Now, my class models data and data relationships, rather than data, data relationships, AND data processes.

Point 2 I think is the most poignant: what is a class? Classes logically group together processes and data in meaningful ways. By anonymizing these functions, I can treat classes as self-documenting snippets of ideas. My __init__() can be nothing more than a series of definitions, manipulations of those definitions to create more definitions, and perhaps some initialization logic that organizes those definitions. Let’s look at another example to illustrate my point more concretely:

class Node:
    def __init__(self, value, next_node=None):
        self.value = value
        self.next = next_node

class TreeNode:
    def __init__(self, value, children=None):
        self.value = value
        self.children = children or []

    def sum_tree_values(self):
        return anon_sum_tree_values(self)

def anon_sum_tree_values(node: TreeNode):
    current = node.value
    total = 0
    while current:
        total += current.value
        current = current.next
    # then recurse for children
    for child in node.children:
        total += sum_tree_values(child)
    return total

Here I’ve built a tree as a LinkedList and used sum_tree_values() to return a sum of all the values in the nodes of the tree. Recursion is an excellent use-case for anon functions; I daresay you should ever recurse on a method. Now, I can easily see that I have a handrolled tree, it has one function to its name, and I know exactly what I need to know and nothing more about this function. 5 years from now, when I look back on this code, I the programmer can quickly injest what’s going on here and move on.

I’m going to shadowbox on this subject and add more to it as I come up with new ideas 🙂 So, keep checking back to read more of my thoughts <3

II. Testing

Writing tests in Python is a relatively intuitive process compared to most languages. I have a few paradigms I want to iron out. The second one I’ll do later, because I want some way to mirror debug outputs in the way Rust does it. But first I want to build upon the last one to show how I like to write tests.

The general idea in writing tests for me is to have tests in a per-file setting. That’s what I think if __name__ == Main is for. Imports should import the classes you’ve built, and should ignore the functions. Tests should go below your helper functions, at the bottom. This is where you can workshop the logic you’re implementing as you do, while writing code that is unavailable to the programmer when they import your work (because they don’t need it). This code should be outright ignored by the LSP if you’re using .’s to tab complete.

Leave a Reply

Your email address will not be published. Required fields are marked *