Your code is easiest to read just after you’ve written it. Your future self will find your code far less readable days, weeks, or months after you’ve written it.
When it comes to code readability, whitespace is your friend.
Whitespace around operators
Compare this:
result = a**2+b**2+c**2
To this:
result = a**2 + b**2 + c**2
I find that second one more readable because the operations we’re performing are more obvious (as is the order of operations).
Too much whitespace can hurt readability though:
result = a ** 2 + b ** 2 + c ** 2
This seems like a step backward because we’ve lost those three groups we had before.
With both typography and visual design, more whitespace isn’t always better.
Auto-formatters: both her…
Your code is easiest to read just after you’ve written it. Your future self will find your code far less readable days, weeks, or months after you’ve written it.
When it comes to code readability, whitespace is your friend.
Whitespace around operators
Compare this:
result = a**2+b**2+c**2
To this:
result = a**2 + b**2 + c**2
I find that second one more readable because the operations we’re performing are more obvious (as is the order of operations).
Too much whitespace can hurt readability though:
result = a ** 2 + b ** 2 + c ** 2
This seems like a step backward because we’ve lost those three groups we had before.
With both typography and visual design, more whitespace isn’t always better.
Auto-formatters: both heroes and villains
If you use an auto-formatter like ruff or black, you generally don’t have control over the whitespace around your operators.
That above example will always end up like this:
result = a**2 + b**2 + c**2
With an auto-formatter you don’t have control over all whitespace in your code, but you do have control over some of it. I’ll note which suggestions below can’t be controlled by you when auto-formatting.
Using line breaks for implicit line continuation
Adding extra line breaks to code can improve readability.
Putting line breaks within braces, brackets, or parentheses in Python creates an implicit line continuation. Python pretty much ignores line breaks within open braces, brackets, and parentheses, the same way it ignores most space characters within a line of code.
If your code is wrapped in a bracket, brace, or parentheses, you can add as many line breaks as you’d like and Python will still treat the code as a single "line".
Breaking comprehensions over multiple lines
I almost always break my comprehensions over multiple lines in Python. I prefer this:
html_files = [
p.name
for p in paths
if p.suffix == ".html"
]
Instead of this:
html_files = [p.name for p in paths if p.suffix == ".html"]
When breaking up code over multiple lines, focus on breaking at logical boundaries rather than at arbitrary line lengths. In that list comprehension above, the mapping part, the looping part, and the conditional part are all on their own lines. When I teach how to write comprehensions, I usually recommend copy-pasting from a loop to a comprehension by separating the mapping, looping, and conditional part of a comprehension each on their own line.
If you use an auto-formatter, you probably don’t have control over this. If your comprehension can fit on one line and still be under your 79 character line length (or gasp even longer!) then you’ll need to suffer through a whole bunch of logic being squeezed onto one line of code.
Breaking a function call over multiple lines
Sometimes I’ll even add parentheses for the sake of an implicit line continuation:
fruits = ["grapes", "apples", "blueberries", "watermelon", "pears"]
print("I like", " and ".join(sorted(fruits)), "but I only like certain pears")
That print line could technically fit within a reasonable line length, but it’s quite dense and hard to parse. Adding parentheses allows us to break it up for better readability:
fruits = ["grapes", "apples", "blueberries", "watermelon", "pears"]
print(
"I like",
" and ".join(sorted(fruits)),
"but I only like certain types of pears",
)
Now the distinct arguments to print are much more obvious.
Also note the trailing comma in that last example. That will force auto-formatters to keep those arguments broken over separate lines.
Wrapping chained method calls in parentheses
When chaining methods together, you may want to add parentheses around the entire expression to allow line breaks between each method call:
books = (Book.objects
.filter(author__in=favorite_authors)
.select_related("author", "publisher")
.order_by("title"))
Here we’ve broken up each method call onto its own line, making it clear what operations we’re performing in sequence.
Of course, how you wrap your code is up to the style your project uses.
I usually prefer this wrapping style:
books = (
Book.objects.filter(author__in=favorite_authors)
.select_related("author", "publisher")
.order_by("title")
)
That’s also the style that black and ruff prefer. And just as with comprehensions, if you use an auto-formatter, you probably don’t have control over this: chained method calls will wrap the way your auto-formatter says they should wrap.
Dictionary, list, set, and tuple literals
Long dictionaries can be broken up with each key-value pair on its own line:
MONTHS = {
"January": 1,
"February": 2,
"March": 3,
"April": 4,
"May": 5,
"June": 6,
"July": 7,
"August": 8,
"September": 9,
"October": 10,
"November": 11,
"December": 12,
}
I sometimes even wrap short dictionaries and lists this way as well, for readability’s sake.
For example that fruits list above:
fruits = ["grapes", "apples", "blueberries", "watermelon", "pears"]
Could have been written like this:
fruits = [
"grapes",
"apples",
"blueberries",
"watermelon",
"pears",
]
I find this slightly more readable.
Also note the trailing comma in these examples. I always use trailing commas when breaking lists, dictionaries, sets, and tuples over multiple lines. I find trailing commas helpful for maintainability and for nudging auto-formatters to keep each item on its own line.
Separating sections with blank lines
PEP8 recommends 2 blank lines between top-level classes and functions and 1 blank line between methods within a class:
import os
class set_env_var:
"""Context manager that temporarily sets an environment variable."""
def __init__(self, var_name, new_value):
self.var_name = var_name
self.new_value = new_value
def __enter__(self):
self.original_value = os.environ.get(self.var_name)
os.environ[self.var_name] = self.new_value
def __exit__(self, exc_type, exc_val, exc_tb):
if self.original_value is None:
del os.environ[self.var_name]
else:
os.environ[self.var_name] = self.original_value
def process_environment():
"""Process environment variables."""
return os.environ.get('PATH')
Although some Python developers prefer to use only 1 blank line between top-level classes and functions instead.
You may also find one or two blank lines between blocks of code useful, either at the global scope:
import subprocess
import sys
process = subprocess.run(
["git", "branch", "--format=%(refname:short)"],
capture_output=True,
text=True,
)
lines = process.stdout.splitlines()
if "main" in lines:
print("main")
elif "master" in lines:
print("master")
elif "trunk" in lines:
print("trunk")
else:
sys.exit("Default branch is unknown")
Or within a function:
def check_stackexchange_urls(ids, id_type, site="stackoverflow"):
"""Return list of invalid IDs from given StackExchange IDs."""
a_url = "https://api.stackexchange.com/2.3/answers/{ids}?site={site}"
q_url = "https://api.stackexchange.com/2.3/questions/{ids}?site={site}"
types = {
"question": (q_url, "question_id"),
"answer": (a_url, "answer_id"),
}
assert id_type in types
url_format, id_key = types[id_type]
url = url_format.format(
ids=";".join(map(str, ids)),
site=site,
)
with urlopen(url) as response:
data = zlib.decompress(response.read(), 16+zlib.MAX_WBITS)
found = {
item[id_key]
for item in json.loads(data.decode())["items"]
}
results = []
for id_ in ids:
url = f"https://{site}.com/{id_type[0]}/{id_}/"
error = None if id_ in found else f"{id_} {id_type} not found"
results.append((url, error))
print("[-]" if error else "[ ]", url)
return results
Usually when I find myself tempted to put a blank line between code blocks, I ask myself whether one or more of the code blocks should live in its own function:
ANSWER_URL = "https://api.stackexchange.com/2.3/answers/{ids}?site={site}"
QUESTION_URL = "https://api.stackexchange.com/2.3/questions/{ids}?site={site}"
TYPES = {
"question": (QUESTION_URL, "question_id"),
"answer": (ANSWER_URL, "answer_id"),
}
def get_valid_ids(ids, id_type, site):
"""Return set of all the valid IDs."""
assert id_type in TYPES
url_format, id_key = TYPES[id_type]
url = url_format.format(
ids=";".join(map(str, ids)),
site=site,
)
with urlopen(url) as response:
data = zlib.decompress(response.read(), 16+zlib.MAX_WBITS)
return {
item[id_key]
for item in json.loads(data.decode())["items"]
}
def check_stackexchange_urls(ids, id_type, site="stackoverflow"):
"""Return list of invalid IDs from given StackExchange IDs."""
found = get_valid_ids(ids, id_type, site)
results = []
for id_ in ids:
url = f"https://{site}.com/{id_type[0]}/{id_}/"
error = None if id_ in found else f"{id_} {id_type} not found"
results.append((url, error))
print("[-]" if error else "[ ]", url)
return results
Some Python developers prefer to use blank lines very liberally. For example, this code has many short blocks of code separated by blank lines:
def group_by_lengths(text):
lengths = {}
for word in text.split():
length = len(word)
if length in lengths:
lengths[length].append(word)
else:
lengths[length] = [word]
I would probably prefer to write that same code like this:
def group_by_lengths(text):
lengths = {}
for word in text.split():
length = len(word)
if length in lengths:
lengths[length].append(word)
else:
lengths[length] = [word]
Some blank lines improve readability, but not all blank lines do.
Which line breaks help and which don’t is very much a matter of personal preference. I tend to err on the side of adding too few blank lines, but I’ve worked with developers who tend to add too many blank lines. Try writing your code a few different ways and see what you prefer.
Note that while auto-formatters will affect blank lines between classes and functions, most blank lines are under your control.
The whitespace is for us, not for Python
Python mostly doesn’t care about whitespace.
That might seem like a strange thing to say, since Python cares about indentation and Python uses line breaks to end a line of code. But most whitespace is for us more than it’s for Python.
Python doesn’t care about:
- newlines between lines
- spaces within a line of code
- newlines or spaces within braces (implicit line continuation)
Python doesn’t care much about our use of whitespace, but humans do. Make your code more readable to other humans by using whitespace wisely.
Consider ruff, black, and other auto-formatters
Also, consider using an auto-formatter. I find that most automatic formatting that ruff and black perform make my code more readable. Unfortunately, some auto-formatting makes my code look bizarre... but that’s the cost of using these tools. Not all automatic reformatting will improve readability.
Even if you do use an auto-formatter, keep in mind that it’s still up to you to decide where you place blank lines between logic sections of code and how you break your code into functions (as I discussed in my article about over-commenting).
Also look into Black’s –preview flag for experimental style changes which might improve your code style and subscribe to issues for potential future changes that interest you (like this one).
Whitespace is all about visual grouping
Where you use whitespace depends on what you want to visually separate or group together in your code.
As you write your code, pause and ask: “what should stand out here at a glance?”
Then use whitespace to make that structure obvious.
Whitespace is your friend — use it wisely.