My Python Programming Style
I’ve been programming in Python for quite a number of years now. Over the years, I’ve developed my own Python “style” that allows me to create beautiful, readable code. This blog post outlines my Python programming style and covers a wide array of topics such as syntax, readability, naming, and more.
Contents
- The Golden Rule
- Base Principles
- Tabs vs Spaces
- Source Code Width
- Grouping
- Imports
- Single vs Double Quotes
- Comments
- Docstrings
- Naming
- The Walrus
- String Formatting
- Run Protection
- Conclusion
The Golden Rule
First and foremost, do not follow style guides. Write code the way that you want to. This is my style, and by no means am I trying to say that this is the superior style to Python programming or that all programmers must or even should follow this.
Base Principles
Programs should first and foremost be functional and efficient. The source code that creates such a program may not necessarily be readable or easy to understand, but it is functional. A program that is functional and efficient while also having readable and easy to understand source code is said to be beautiful. Always prioritize function and form, but where it is impossible, function is more important than form. Ultimately, programs are made to serve the users, not the programmers.
Python code should resemble English to some extent. Every single line of code must logically make sense. If it does not logically make sense, then it is not good code. Step back and review the logic until it makes sense, or rewrite it using logic that does make sense. Only then can you continue.
More code is better than less code if it allows a programmer to better express their logic while also not impacting the functionality or efficiency of a program. However, if using less code makes more sense, then less code should be used.
Python is a tremendous language with a great number of features. These features should only be used if they make more logical sense. Otherwise, there’s no point in using features that have little to no recognizability. One-liners can be elegant, but only if they make sense. Keep it simple, stupid!
Tabs vs Spaces
Tabs should be used over spaces for indentation. Tabs syntactically denote indentation, while spaces do not. Using tabs allows a programmer to not have to focus on counting indentation.
I understand that the primary argument for using spaces for indentation is that it results in consistent code across systems since tab widths may be different from system to system. I hear this, and I raise a counterargument: using tabs instead of spaces allows a programmer to define their own tab widths. There are arguments that indentation should be 8 characters, 4 characters, or even 2 characters in width. By using tabs for indentation, you respect others’ choices by allowing them to specify their own tab widths and letting their system display it to their preference.
Source Code Width
Any line of source code should not exceed 82 characters in width. The reason why I do not exceed 82 characters in width is because I primarily use a 1366x768 resolution monitor with 14px Source Code Pro font, meaning that I can have two tiles, one for the source code at 82 characters in width and one for running and debugging at 62 characters in width, side-by-side without any horizontal scrolling.
Grouping
Like ideas should be grouped together. Groups should be separated from other groups by line breaks.
# This is an excerpt from some data visualizations I did a while ago.
if len(args) != 2:
print("Usage: ./analyze.py <snow json> <employees json>")
return -1
with open(args[0], "r") as f:
data = json.loads(f.read())
with open(args[1], "r") as f:
employees = json.loads(f.read())
What qualifies as a group of like ideas is completely up to the programmer. Like ideas may be nested inside of each other, such as is common with conditionals and loops. In this case, nested groups should still be separated from other nested groups, and the parent group that they belong to should still be separated from other groups on the same level.
# This for loop and all its content is one group.
for Node in nodes:
# This conditional is a nested group with other nested groups inside of it.
if Node.isAlive():
# Nested group.
Node.execute()
# Nested group.
with open("dstream.dat", "a+") as f:
f.write(Node.data)
# This is in the same group as the "if" from above, but is spaced from the
# nested group immediately above this comment.
else:
deadNodeCount += 1
# This conditional and all its content is another group.
if deadNodeCount > 0:
print("Some dead nodes were detected.")
Imports
Imports should be done in alphabetical order. Modules should be separated from the other imports with a line break. Avoid doing from foo import *
wherever possible. Using from foo import bar
is okay as long as namespace collisions are avoided, and these imports should be separated from the other imports with a line break, but should appear before module imports.
In the main Python program, imports should appear at the top of the code. In any auxiliary modules, they should only appear in the function or method that they are used in. Auxiliary modules should be categorized into their own folders. For these auxiliary modules, it’s okay to do import foo.bar as bar
when they’re being imported so as long as namespace collisions are avoided.
# Suppose that this is in the main program.
import bs4
import requests
from time import sleep
import lib.foo as foo
import aux.bar as bar
# Suppose that this is in an auxiliary module.
def type_out(text):
import sys
import time
for char in text:
sys.stdout.write(char)
sys.stdout.flush()
time.sleep(0.01)
print()
Single vs Double Quotes
Double quotes should be used for strings and single quotes should be used for characters, except for where it is visually easier to understand as is in the case of nested quotes.
single = 'c'
double = "Hello world!"
nested = 'He said, "Pack your bags!"'
Comments
Comments should be concise and succinct. Save long comments for documentation unless absolutely necessary. Comments should only be used to clarify potentially confusing parts of a program or perhaps summarizing chunks of instructions. Otherwise, the source code should speak for itself. The amount of comments your program should have should be inversely proportional to the Python proficiency of whomever you expect to view your code.
# This is an excerpt from some networking stuff I did a while ago.
# Read device IP addresses from settings.IP_LIST_LOCATION.
if os.path.exists(settings.IP_LIST_LOCATION):
ips = [l.strip("\n") for l in open(settings.IP_LIST_LOCATION, "r").readlines()]
else:
log(f"Could not find file '{settings.IP_LIST_LOCATION}'")
return -1
# A holder for the object soon-to-be instantiated.
ICX = None
Comments should always be on their own line unless they are following a collection of instructions and comments in series, in which case they should follow the instructions and be aligned with other adjacent comments. Regardless, try to minimize horizontal scrolling by not exceeding approximately 82 characters in width. If your comment exceeds this width, try breaking it up into multiple comments. Comments should always be at the same indentation level as a regular instruction where it is written.
username = input("Name: ") # Ask the user for their username.
password = getpass.getpass() # Ask the user for their password.
if (username == "AzureDiamond") and (password == "hunter2"):
# This is a nested comment inside a conditional.
# Notice how it's at the same indentation as the code inside of here.
print("This is an old-school meme.")
else:
Account.save(username, password)
Docstrings
Do not use comments to describe functions, methods, or classes. Use docstrings instead. Docstrings should use double quotes instead of single quotes, as single quotes should only be used for characters. Docstrings should briefly describe the function or method as well as give an example of its usage. Avoid overly verbose docstrings. Save the true documentation for the actual documentation!
# This is an excerpt from some networking stuff I did a while ago.
def install_image(self, tftpServerIP, imageName, destination):
"""
Install a new image from a TFTP server.
e.g. ICX.install_image("10.0.0.155", "boot_image.bin", "boot")
"""
try:
return self.send_cmd(
f"copy tftp flash {tftpServerIP} {imageName} {destination}",
"TFTP to Flash Done."
)
except:
return -1
Naming
Variables, attributes, and parameters should be named in lower camel case; functions and methods in snake case; classes and objects in Pascal case; and constants in screaming snake case. The only exceptions should be if the name of something is explicitly in some other case, such as in acronyms or initialisms like NASA or ICX. Regardless, each of these should be named as descriptively as possible while also not being obnoxiously verbose.
# Variables
myName = "Shawn Duong"
# Functions and parameters
def greet_person(personName):
print(f"Hello {personName}!")
# Functions
greet_person(myName)
# Classes
class Person:
# Attributes
name = None
# Methods and parameters
def __init__(self, personName):
self.name = personName
# Objects
ShawnDuong = Person(myName)
# Constants
GRAVITY = 6.674e-11
The Walrus
The walrus operator, :=
, was a new operator introduced in Python 3.8 and is, from my perspective, very elegant when used correctly. The walrus should be incorporated into code wherever logical.
if (answer := input("> ")).lower() == "a":
print("I am the walrus.")
String Formatting
The most elegant way to format a string in Python is using an fstring, and they should therefore be used as much as possible. If an fstring cannot be used, then a C-style string format should be used. Only as a last preference should string concatenation be used. The .format()
method should never be used as it has been eclipsed by fstrings.
name = "Shawn Duong"
print(f"Hey, my name is {name}!")
x = 4.8921
print(f"Float with 2 decimals: {x:.2f}")
Run Protection
Something that should always be used in the main program of a Python project is run protection. “Run protection” is what I’ve taken to calling the if __name__ == "__main__"
instruction. Having this in your program allows you to import instructions without necessarily having to run them, making debugging much less of a hassle as you can explicitly choose when to execute the program’s instructions and how many times to do so.
def main():
print("foobar")
if __name__ == "__main__":
main()
Conclusion
Well, that’s the basic gist of my programming style in Python. As a final note, I’d like to remind you not to follow style guides and instead program how you see fit. It’s important to prioritize function and form.
Happy hacking!