Python style guide

This document describes expected practices when writing Python code. There are occasions when you can break these rules, but be prepared to justify doing so when your code gets reviewed.

Existing Conventions

There are well-established conventions in the Python community, and in general we should follow these. General Python conventions, and required reading:

  • PEP 8: Style Guide for Python Code

  • PEP 257: Docstring Conventions

  • The Zen of Python: python3 -c "import this"

Note that our standards differ slightly from PEP-8 in some cases.

Coding standards other projects use:

Formatting

We delegate most formatting decisions to black. All Python code (except for a few files specifically excluded in .pre-commit-config.yaml) must be formatted using it. You should be using Launchpad’s default pre-commit setup, which automatically formats your code using black and isort before you commit.

Naming

Consistency with existing code is the top priority. We follow PEP-8 with the following exceptions:

  • CamelCase: classes, interfaces (beginning with I)

  • initialCamelCase: methods

  • lowercase_underscores: functions, non-method attributes, properties, local variables

  • ALL_CAPS: constants

Private names are private

You should never call a non-public attribute or method from another class. In other words, if class A has a method _foo(), don’t call it from anywhere outside class A.

Docstrings

  • If you haven’t already, read PEP 257.

  • In general, everything that can have a docstring should: modules, classes, methods, functions.

  • Docstrings should always be enclosed in triple double quotes: """Like this."""

  • When a class or a method implements an interface, the docstring should say """See `IFoo`."""

Docstrings should be valid reStructuredText (with all the painful indentation rules that implies) so that tools such as pydoctor can be used to automatically generate API documentation.

You should use field names as defined in the epydoc documentation but with reST syntax.

Using `name` outputs a link to the documentation of the named object, if pydoctor can figure out what it is.

Here is a comprehensive example. Parameter descriptions are a good idea but not mandatory. Describe in as much or as little detail as necessary.

def example2(a, b):
    """Perform some calculation.

    It is a **very** complicated calculation.

    :param a: The number of gadget you think this
              function should frobnozzle.
    :type a: ``int``
    :param b: The name of the thing.
    :type b: ``str``
    :return: The answer!
    :rtype: ``str``.
    :raise ZeroDivisionError: when ``a`` is 0.
    """

Modules

Each module should look like this:

# Copyright 2009-2011 Canonical Ltd.  All rights reserved.

"""Module docstring goes here."""

__all__ = [
    ...
]

The file standard_template.py has most of this already, so save yourself time by copying that when starting a new module. The “…” should be filled in with a list of public names in the module.

PEP-8 says to put any relevant __all__ specifications after the module docstring but before any import statements (except for __future__ imports, which in most cases we no longer use). This makes it easy to see what a module contains and exports, and avoids the problem that differing amounts of imports among files means that the __all__ list is in a different place each time.

Imports

Restrictions

There are restrictions on which imports can happen in Launchpad. Namely:

  • View code cannot import code from lp.*.model.

  • import * cannot be used if the module being imported from does not have an __all__.

  • Database code may not import zope.exceptions.NotFoundError – it must instead use lp.app.errors.NotFoundError.

These restrictions are enforced by the Import Pedant, which will cause your tests not to pass if you don’t abide by the rules.

Use absolute imports (from foo.bar import Bar), not relative imports (from .bar import Bar).

Import scope

We encourage importing names from the location they are defined in. This seems to work better with large complex components.

Circular imports

With the increased use of native Storm APIs, you may encounter more circular import situations. For example, a MailingList method may need a reference to the EmailAddress class for a query, and vice versa. The classic way to solve this is to put one of the imports inside a method instead of at module global scope (a “nested import”).

Short of adopting something like Zope’s lazy imports (which has issues of its own), you can’t avoid this, so here are some tips to make it less painful.

  • Do the nested import in the least common case. For example, if 5 methods in model/mailinglist.py need access to EmailAddress but only one method in model/emailaddress.py needs access to MailingList, put the import inside the emailaddress.py method, so you have fewer overall nested imports.

  • Clearly comment that the nested import is for avoiding a circular import, using the example below.

  • Put the nested import at the top of the method.

def doFooWithBar(self, ...):
    # Import this here to avoid circular imports.
    from lp.registry.model.bar import Bar
    # ...
    return store.find((Foo, Bar), ...)

Circular imports and webservice exports

One of the largest sources of pain from circular imports is caused when you need to export an interface on the webservice. Generally, the only way around this is to specify generic types (like the plain old Interface) at declaration time and then later patch the webservice’s data structures at the bottom of the interface file.

Fortunately there are some helper functions to make this less painful, in lib/lp/services/webservice/apihelpers.py. These are simple functions where you can give some info about your exported class/method/parameters and they do the rest for you.

For example:

from lp.services.webservice.apihelpers import (
    patch_entry_return_type,
    patch_collection_return_type,
)
patch_collection_return_type(
    IArchive, "getComponentsForQueueAdmin", IArchivePermission
)
patch_entry_return_type(IArchive, "newPackageUploader", IArchivePermission)

Properties

Properties are expected to be cheap operations. It is surprising if a property is not a cheap operation. For expensive operations use a method, usually named getFoo(). Using cachedproperty provides a work-around but it should not be overused.

Truth conditionals

Remember that False, None, [], and 0 are not the same although they all evaluate to False in a boolean context. If this matters in your code, be sure to check explicitly for either of them.

Also, checking the length may be an expensive operation. Casting to bool may avoid this if the object specializes by implementing __bool__.

Chaining method calls

Since in some cases (e.g. class methods and other objects that rely on descriptor __get__() behaviour) it’s not possible to use the old style of chaining method calls (SuperClass.method(self, ...)), we should always use the super() builtin when we want that.

Note

The exception to this rule is when we have class hierarchies outside of our control that are known not to use super() and that we want to use for diamond-shaped inheritance.

Use of lambda, and operator.attrgetter

Prefer operator.attrgetter to lambda. Remember that giving functions names makes the code that calls, passes and returns them easier to debug.

Creating temporary files

We should use the most convenient method of the tempfile module. Never taint /tmp/ or any other “supposed to be there” path.

Despite being developed and deployed on Ubuntu systems, turning it into a restriction might not be a good idea.

When using tempfile.mkstemp remember it returns an open file descriptor which has to be closed or bound to the open file, otherwise they will leak and eventually hit the default Linux limit (1024).

There are two good variations according to the scope of the temporary file.

fd, filename = mkstemp()
os.close(fd)
...
act_on_filename(filename)

Or:

fd, filename = mkstemp()
with os.fdopen(fd, "w") as temp_file:
    ...
    temp_file.write("foo")

Never use:

fd, filename = mkstemp()
with open(filename) as temp_file:
    temp_file.write("foo")
# BOOM! 'fd' leaked.

In tests, you should use the TempDir fixture instead, which cleans itself up automatically:

from fixtures import TempDir

class TestFoo(TestCase):
...
    def test_foo(self):
        tempdir = self.useFixture(TempDir).path
        ...
        do_something(os.path.join(tempdir, "test.log"))
        ...

Configuration hints

Vim

To make wrapping and tabs fit the above standard, you can add the following to your .vimrc:

autocmd BufNewFile,BufRead *.py set tw=78 ts=4 sts=4 sw=4 et

To make trailing whitespace visible:

set list
set listchars=tab:>.,trail:-

This will also make it obvious if you accidentally introduce a tab.

To make long lines show up:

match Error /\%>79v.\+/

For an even more in-depth Vim configuration, have a look at UltimateVimPythonSetup for a complete vim file you can copy to your local setup.

Emacs

There are actually two Emacs Python modes. Emacs comes with python.el which has some quirks and does not seem to be as popular among hardcore Python programmers. python-mode.el comes with XEmacs and is supported by a group of hardcore Python programmers. Even though it’s an add-on, it works with Emacs just fine.