Mind Your Brackets

Sep 7, 2017 08:00 · 1466 words · 7 minutes read code style

Those who work with me know that I am an advocate of 80 character line limits. This comes naturally as a Python developer and I apply the same limit to Javascript, HTML, Dockerfiles, Markdown, etc. This post won’t try to convince you to respect the 80 char limit (though you should), it will cover one of the main challenges of writing code with a length constraint: What do I do when I reach the end of the line and I still have code to write?

How should long dictionaries, lists, tuples and lengthy function arguments be handled? Do you simply hit enter at the end of the line and hope for the best?

The answer is obviously no, yet developers, myself included, often struggle or overlook the importance of practising consistent line continuation techniques and the impact on overall readability and health of a codebase.

Style guides typically don’t help us answer these questions or give us a clear pattern, so below are 3 rules which capture well continued code.

The rules

  1. Balance the brackets. Brackets and parentheses should be visually symmetric and easily identifiable.

  2. Avoid unnecessary identation. Deep indentation decreases readability.

  3. Put every element on its own line. This makes it faster to read, and easier to maintain lists.


1. Balance the brackets

When spanning multiple lines, the opening bracket should be the final character of the row and the closing bracket should be on a new line and match the indentation of the first character on the line containing the opening bracket.

// Good
if (something) {
    console.log('fizz');
}

// Bad
if (something) {
    console.log('fizz'); }

This should be a familiar concept and standard practice in many languages, yet we overlook this detail when calling functions with long lists of arguments, or when defining arrays, dictionaries and tuples. The symmetry provided by indentation and the isolation of the brackets helps our eyes delineate different blocks of code.

Consider the following example:

# Bad
cities = [
    {'id': 1, 'name': 'Montreal'},
    {'id': 2, 'name': 'New York'}]

Here, like in the earlier JS example, the closing bracket is out-of-balance in relation to the opening one. Even in this short snippet, the cities list feels like it is dangling uncomfortably.

# Good
cities = [
    {'id': 1, 'name': 'Montreal'},
    {'id': 2, 'name': 'New York'}
]

This next example demonstrates a confusing chain of closing parentheses following the choices keyword-argument.

# Bad
country = models.CharField(
    null=True,
    blank=True,
    help_text='The name of the country',
    choices=((1, 'Canada'),
             (2, 'US')))
city = models.CharField(...)

At a quick glance, it is difficult to match the closing parentheses with their opening ones. By following the if/else example earlier, let’s place the opening parentheses at the end of each declarative line, and the closing ones is on their own, outdented lines.

# Good
country = models.CharField(
    null=True,
    blank=True,
    help_text='The name of the country',
    choices=(
        (1, 'Canada'),
        (2, 'US')
    )
)
city = models.CharField(...)

While this revised snippet does add 2 extra lines of code, we can now identify the beginning and end of the CharField arguments and of the choices tuple.


2. Avoid unnecessary indentation

Always put the first element/argument on a new line, never after an opening bracket.

The first rule pretty much achieves this, since we end lines by an opening bracket, not by an argument, yet the rationale is different.

Let’s consider the following statement from the Zen of Python (which is always available by typing import this into the python interpreter):

Flat is better than nested

Flat code tends to be easier to read than nested in part because it encourages a simpler structure. Flat code also tends to be less indented, and therefore more readable as the eye has less distance to travel.

Returning early from functions, or raising exceptions early is a good way to to avoid nesting and keep indentation at a minimum, but it is still easy to introduce unnecessary hanging indents to code. One culprit is the argument following the opening bracket on the same line.

# Bad:
queryset = MyVerboseModelName.objects.filter(some_property=some_value,
                                             something_else=None,
                                             some_other_property=other_value)


#   #   #   #   #   #   #   #   #   #   #   #|  <- 45 cols wasted !!

The code above is awkward. The eye has to travel far, particularly within the context of other deeply nested code.

# Worse:
queryset = MyVerboseModelName.objects.filter(some_property=some_value,
                                             something_else=None,
                                             some_other_property=other_value)
queryset2 = MyOtherClass.objects.filter(fizz='buzz',
                                        foo='bar')

By putting each element on its own line, unnecessary indentation is avoided.

# Good
queryset = MyVerboseModelName.objects.filter(
    some_property=some_value,
    something_else=None,
    some_other_property=other_value
)
queryset2 = MyOtherClass.objects.filter(
    fizz='buzz',
    foo='bar'
)
#  ♥‿♥

Now, back to our “bad” example. Let’s see why, on top of being hard to read, this code is also hard to maintain.

# Bad:
queryset = MyVerboseModelName.objects.filter(some_property=some_value,
                                             something_else=None,
                                             some_other_property=other_value)

Say we want to rename queryset to q; what happens to the indentation of the following lines?

# Bad
q = MyVerboseModelName.objects.filter(some_property=some_value,
                                             something_else=None,
                                             some_other_property=other_value)
#   #   #   #   #   #   #   #   #   #  ..... <- Invalid indentation!

The following arguments are now improperly indented, and we now have to touch 3 lines of code rather than just one. This leads to noisy and confusing diffs, and increases the risk of unnecessary merge conflicts should these lines be modified in another branch.

By placing the first argument some_property on its own line, changes to the variable named q will only affect one line.

# Good
queryset = MyVerboseModelName.objects.filter(
    ...
)

q = MyVerboseModelName.objects.filter(
    ...  # Always the same indentation level
)

3. Put every element on its own line

Putting each element on its own line tremendously improves readability and code maintainability. Vertical real-estate on the screen is cheap, horizontal space is limited.

# Bad
fields = ('first_name', 'last_name', 'primary_phone', 'secondary_phone',
          'fax', 'company_name', 'address', 'address2', 'city', 'state',
          'country', 'email')

Sure, this code it fits within 80 chars, but this code is problematic for the following reasons:

  1. It is difficult / slow to read

    How do you typically write a grocery list or a todo list? Do you list your elements as though they form a sentence in a paragraph, or do put every item on its own line?

    I’d argue most people put each of their list items on their own line, and that this format is more readable.

    The same applies to code!

  2. It is difficult to maintain.

    Say you are required to add a new field to the list called middle_name and place it between first_name and last_name. Do you simply insert it?

    What if you have to obey the 79-char limit and this new field now causes you to exceed the maximum line length? Do edit the entire list to make it fit – in this case, 3 lines?

    What if someone on your team also has an item to add to the list and also rearranges the placement of each item? Hello difficult-to-fix merge conflict!

The solution? Put every item on its own line.

fields = (
    'first_name',
    'last_name',
    'primary_phone',
    'secondary_phone',
    'fax',
    'company_name',
    'address',
    'address2',
    'city',
    'state',
    'country',
    'email'
)

Most of these examples are Python, but this works well in other languages and even in HTML, particularly with elements having long lists of attributes.

<!-- Bad -->
<div class="field">
  <label for="city">City</label>
  <input name="city" id="city" class="really awesome text input" type="text" placeholder="Toronto" value="Gotham" required>
</div>

In addition to the horrible experience of having to scroll horizontally to view then entire markup, changes to the <input> markup will generate a diff which is difficult to read. Code that is laborious to read makes it harder to catch mistakes during reviews and therefore, easier to introduce bugs.

<!-- Good -->
<div class="field">
  <label for="city">City</label>
  <input
    name="city"
    id="city"
    class="really awesome text input"
    type="text"
    placeholder="Toronto"
    value="Gotham"
    required
  >
</div>

Placing every element on its own line allows us to view the <input> markup in its entirety. We can add, remove and modify attributes cleanly, and since the changes are isolated, comparing diffs becomes a more pleasant experience. Your teammates will appreciate this when reviewing your pull requests!

Final thoughts

The objective of this post is to underline the importance of having a consistent approach to managing code that needs to span multiple lines.

Take look at your own code, or a popular open source project and ask the following questions:

  • How does the code flow?
  • Is it easy to find opening and closing brackets?
  • Is the code close to the left margin or is there a lot whitespace that causes your eyes to scan horizontally, or worse, force you to scroll horizontally?
  • How are long lists and functions with lots of arguments organized? Are items placed on their own lines, or are they packed into a paragraph?

Being mindful of the rules suggested in this post will help improve overall code readability and maintainability. Having an objective and methodological approach to organizing the code also takes the guess work out of splitting code onto multiple lines.

Happy coding!