Vertical spacing is important for readability reasons: group together pieces of code that should not be split apart, and otherwise add blank lines among chunks of code that could be easily reordered and/or repurposed. That's a pretty loose suggestion though, so let's look at some specific situations in which you want to consider your vertical spacing practices (both adding and removing).
Give some air to long functions
Functions longer than a handful lines generally deserve some vertical spacing. A first guideline is to separate the return of the result value (along all of its boilerplate) from everything else that the function may be doing.
Cluster together computations or assignments that are related
When populating the contents of a data structure or when defining multiple variables that are supposed to be used together, do not add blank lines among these lines:
settings = FileOpenSettings()
settings.deadline = 30
settings.allow_failure = True
arguments = FileOpenArguments()
arguments.file_name = '/my/document'
result = server.FileOpen(settings, arguments)
The example above represents a fictitious RPC call to a remote server to open a file. The construction of the RPC settings and the arguments belong in different data types, so we construct each object in its own "code paragraph". Note that there are blank lines to separate the construction of each of these but there are no blank lines to separate the various assignments to the same data structure.
Surround conditionals and loops with one blank line, except for their "prologue" or "epilogue"
Every time I write a conditional or a loop, I start by adding one blank line before and after them. Most times, all is good and the code stays this way. However, there are times where some code needs to be put right at the beginning of the conditional or loop (the prologue), or right after (the epilogue). In those cases, these extra pieces of code are not split apart from the conditional or loop they relate to.
A common occurrence of this is when a loop has two exit conditions: one where an error has been detected and one where everything is OK. When the loop is terminated, you need to inspect which of the two conditions happened and act accordingly.
Take a look at this code, which implements a simple function to classify a set of students by the grade they belong to and to raise an error when one or more students are registered to an unknown grade:
def classify_by_grade(students, known_grades):
students_by_grade = collections.defaultdict(set)
unclassified_students = set()
for student in students:
if student.grade in known_grades:
if len(unclassified_students) == 0:
Regarding spacing, we can see that there are three different sections in the code: the definition of the return value, the computation of the value, and the termination of the function. This is usually a good organization.
However, what I want you to pay attention to is the epilogue and prologue of the loop. Because we wanted to report all the students with an unknown grade (instead of just the first one), we had to accumulate these in an auxiliary variable unclassified_students. The existence of this variable, and the later inspection of it, is for the sole purpose of the loop; therefore, such code must be attached to the loop itself without any vertical spacing in between.
If I were to write this same code in a language with strict scoping rules, I'd even put the loop and the definition of unclassified_students in its own block so that the variable was unaccessible after we were done with it.
Group comments with the code they belong to.
Usually, comments are tied to a specific chunk of code. Separate the comment and such code from the rest with blank lines. For example:
test_person = Person(
# The name of this test person has to be two words long
# at least.
# We want this person to be an adult, so ensure the test age
# is above 21 years old. Granted, this is only true in some
# countries but our code assumes 21.
Note the blank line right before the comment attached to the assignment to age; it is important. Otherwise, I'd assume that such assignment is somehow related to the assignment to name. Another example on these lines:
def __init__(self, name, age):
# Initialize fields that are copies of the input
self.name = name
self.age = age
# Now initialize fields that are derived from the input
# arguments. Yes, yes, believe me when I say that this
# is very wrong, but it serves to illustrate the
self.first_name = name.split(' ')
self.last_name = name.split(' ')[1:]