WriteLab Python Style Guide

As an engineer working at a company focused on writing, I feel that style is as important in programming as it is in writing. The primary motivation for a consistent programming style is readability; others should be able to immediately see what your code is doing, which is difficult when people write their code in different styles. Since our server-side stack is primarily Python, it's appropriate that we have a standard for that language. This guide is based on PEP8 with a few modifications (it should still pass the PEP8 linter, though) and is split up into four main parts: formatting, Python conventions, documentation, and refactoring.

Formatting

79 Character Line Length

Always limit lines to 79 characters as per PEP8, so that multiple files can be opened side-by-side without wrapping occurring. The same goes for docstrings and comments (PEP8 limits these to 72 characters, but the linter doesn't enforce it).

4 Space Indents

Always use 4 spaces for indents. This helps code look the same everywhere, independent of individual tab settings. Almost every editor should have an option to make 4 spaces behave as tab characters when inserting or deleting tabs (and if it doesn't, consider using a different editor *cough*Vim*cough*).

Whitespace

Spaces Around Operators And Characters

The appropriate amount of spaces should be used throughout so that code doesn't look cluttered:

  • There should always be a single space around operators (==, +, and, or, etc.), except for keyword arguments.
  • Always leave a space after commas as well, unless it's the last character of a line (see No Trailing WhiteSpace below).
  • Keep a space between # and the text of comments, and two spaces between code and # for inline comments.

For example:

  • variable = some_function(arg=arg) + 3   # This does something

No Trailing Whitespace

Although it's not usually visible in editors, trailing whitespace is tracked in diffs, which adds unnecessary noise. I wish every editor by default would display trailing whitespace in bright red so everyone could see how ugly it looks.

Separate Statements

Always keep statements on separate lines (including if-else) and never use semicolons, again so that code doesn't look cluttered.

Single Quote Strings

Although PEP8 recommends using double quotes for strings containing single quotes, always use single quotes for consistency and use backslashes to escape single quotes in strings.

Use Parentheses With print

print was changed from a statement to a function in Python 3. When dealing with Python 2 code, include the parentheses to match Python 3.

Breaking Up Long Lines

Multiple Items Per Line

When breaking up long lines containing braces, brackets, or parentheses, the linebreak should be after those characters with the rest of the statement indented on the next line. For example:

  • example_function(
  • argument_one, argument_two, argument_three)

Although PEP8 allows the code below as well, it's discouraged because of the extra space needed for alignment:

  • example_function(argument_one, argument_two,
  • argument_three)

One Item Per Line

When you have more than 3 items and the item names are fairly long, consider putting each argument on a separate line followed by a comma (trailing commas in a function call are allowed in Python), and moving the closing character to its own line. This makes diffs easier to read when items are added or removed. For example:

  • example_function(
  • some_long_argument_name_one,
  • some_long_argument_name_two,
  • some_long_argument_name_three,
  • some_long_argument_name_four,
  • )

Using Parentheses To Wrap Statements

If there are no braces, brackets, or parentheses, you can wrap parentheses around parts of the statement to break it up. For example:

  • variable_one = (
  • variable_two + variable_three + variable_four)

Sort Import Statements

Import statements should be sorted in alphanumeric order (capitals first, then lowercase) by package name and then by module name. Module imports without packages should precede the rest. This convention makes it easier to find a specific module when scanning a list. For example:

  • import os
  • from collections import defaultdict
  • from itertools import count
  • from itertools import izip

File Structure

Files should always follow this format (with the exact amount of newlines separating code blocks) for consistency:

  • import os   # Standard library imports
  • from third.party import some_function   # Third party imports
  • from our.application import SomeClass   # Application imports
  • SOME_CONSTANT   # Constants and global variables
  • def another_function():
  • pass
  • class AnotherClass(object):
  • pass

Python Conventions

Names

For consistency use the following naming conventions:

  • CONSTANT_NAME = 1
  • def function_name(variable_name):
  • return variable_name + CONSTANT_NAME
  • class ClassName(object):
  • attribute_name = 0
  • def method_name(self, variable_name):
  • self.attribute_name = function_name(variable_name)

Use str.format

Always use str.format instead of the % operator to format strings because it's more powerful and expressive. For less than three arguments they can be unnamed, but for three or more, name them. For example:

  • '{} by {}'.format(song, artist)
  • '{song} from {album} by {artist}'.format(song=song, album=album, artist=artist)

Imports

Absolute Imports

Always use absolute imports, as they convey more information about what's being imported. You shouldn't have to know the path of the file to figure out where the module is from.

No Wildcard Imports

Never use wildcard imports unless absolutely necessarily, e.g. you need every single item from the module and you can't reference them with the module name in front of it. Wildcard imports make linting more difficult and may unintentionally cause namespace collisions since what's being imported is unknown.

Import Every Individual Item

Keep each import on a separate line, even for items imported from the same module (don't use commas), so that diffs are nicer when items are added or removed. You should normally always import every individual item from a module, but there are cases where importing the module itself is preferred:

  • You're importing 4 or more items.
  • The number of items you're importing is likely to change in the future.
  • It isn't clear what the item is doing without the module name (e.g. json.read).

Renaming

Modules and items should only be renamed when absolutely necessary (e.g. to avoid namespace collisions). Otherwise, it's not immediately clear what something is without having to look at its import statement.

Documentation

Comments

Use comments where appropriate, especially for large code blocks. For consistency, follow these conventions:

  • Always capitalize the first letter.
  • Comments should consist of full sentences, except possibly when describing what a variable is for.
  • For a single sentence the period is unnecessary, but for multiple sentences it is required.
  • Inline comments should generally be avoided, unless the comment is short and describes a variable.

For example:

  • some_list = []   # A list of items
  • # Populate the list with random numbers from 0 to 10
  • populate(some_list)
  • # Iterate through each item in the list and multiply it by 2. Also check
  • # whether the new value is greater than 10.
  • for i, item in enumerate(some_list):
  • new_item = item * 2
  • if new_item > 10:
  • print('Greater than 10')
  • some_list[i] = new_item

Docstrings

Ideally, every class, method, and function should have a docstring. Again for consistency, follow these conventions:

  • Use three double quotes """ for docstrings to distinguish them from strings.
  • Always capitalize the first letter and use full sentences with punctuation at the end.
  • When everything fits on one line, keep the opening and closing triple quotes on the same line.
  • In contrast to the stricter guidelines of PEP257 (Docstring Conventions) for multiline docstrings, a multiple-line summary with the closing triple quote on its own line will suffice.

For example:

  • def some_function():
  • """Does something."""
  • pass
  • def some_complicated_function():
  • """Does something complicated. Involves some complicated pieces of code
  • which should also be commented.
  • """
  • pass

Refactoring

Data Structures

Know the basic Python data structures and when to use one over the other. Ask yourself questions such as:

  • Do I need to add or remove anything?
  • Do I need to check whether something exists?
  • Do I only need to look at one item at a time?

Try-Except Vs. If-Else

Try-except blocks are preferred to if-else blocks where appropriate (e.g. getting an object and doing something with it). It's easier to ask for forgiveness than permission. Always specify the exact error(s) so that other unintended errors aren't hidden. For example:

  • try:
  • do_something(some_dictionary[some_key])
  • except KeyError:
  • pass

is preferred to:

  • if some_key in some_dictionary:
  • do_something(some_dictionary[some_key])

Don't Copy And Paste

If you find yourself writing similar code with slight modifications, strongly consider refactoring to cut down on code duplication.

Accessing Methods And Attributes with getattr

When you have distinct blocks of code where the only differences are what attributes/methods in a class are used, you can utilize getattr to access them by a variable name. For example:

  • getattr(SomeClass, method_name)(argument)

Decorators For Similar Functions/Methods

Decorators allow functions to be easily wrapped in other functions, which is useful if those functions do similar things at the beginning or end. For example:

  • def decorator(function):
  • def wrapper(*args, **kwargs):
  • # Run stuff before the function
  • function(*args, **kwargs)
  • # Run stuff after the function
  • return wrapper
  • @decorator
  • def function(argument):
  • pass

Mixins For Similar Classes

Python allows for multiple inheritance via mixins, which means you can create mixins with attributes and implemented methods that other classes can use. For example:

  • class MixinOne(object):
  • attribute = 1
  • def some_method(self):
  • return self.attribute + 1
  • class MixinTwo(object):
  • def another_method(self, number):
  • print(number)
  • class SomeClass(MixinOne, MixinTwo):
  • def run(self):
  • self.another_method(self.some_method())

The Problem of Plagiarism

Evaluating our Algorithms