Skip to content

Latest commit

 

History

History
924 lines (719 loc) · 29.5 KB

README.md

File metadata and controls

924 lines (719 loc) · 29.5 KB

Python Style Guide

This style guide aims to document my preferred style for writing Python code.

It is based on Python PEP 8. Portions of this guide borrow heavily from:

Goals

The ultimate goal of this guide is having code that is clean, consistent, and efficient. Some parts of the guide are opinionated and meant to be strictly followed to preserve consistency when writing new code.

Table of Contents

Layout

Indentation

  • Use 4 spaces per indentation level. [link]

  • Continuation lines should align wrapped elements either vertically using Python's implicit line joining inside parentheses, brackets and braces, or using a hanging indent.[link]

    # Yes
    
    # Aligned with opening delimiter.
    def long_function_name(var_one,
                           var_two,
                           var_three,
                           var_four):
    
    # Hanging indents should add a level.
    foo = long_function_name(
        var_one,
        var_two,
        var_three,
        var_four,
    )
    # No
    
    # Arguments on first line are forbidden when not using vertical alignment.
    foo = long_function_name(var_one, var_two,
        var_three, var_four)
    # Good when it fits the line length limit
    def create_translation(phrase_id, phrase_key, target_locale):
        ...
    
    translation = create_translation(phrase_id, phrase_key, target_locale)
    
    # Good, but use it only for function definitions
    def create_translation(phrase_id,
                           phrase_key,
                           target_locale,
                           value,
                           user_id,
                           do_xss_check):
        ...
    
    # Good, stick to one argument or element per line
    # This applies to lists, tuples, sets, dictionaries, function calls
    translation =  create_translation(
        phrase_id,
        phrase_key,
        target_locale,
        value,
        user_id,
        do_xss_check,
    )

Inline

  • Never leave trailing whitespace. [link]

  • Avoid extraneous whitespace in the following situations.[link]

  • Don't use spaces around the = sign when used to indicate a keyword argument or a default parameter value.[link]

    # No
    def func(arg1, arg2 = 2):
    
    # Yes
    def func(arg1, arg2=2):
  • Compound statements (multiple statements on the same line) are generally discouraged.[link]

    # Yes
    
    if foo == 'blah':
        do_blah_thing()
    do_one()
    do_two()
    do_three()
    
    # Rather not
    
    if foo == 'blah': do_blah_thing()
    do_one(); do_two(); do_three()
    
    # Definitely not
    
    if foo == 'blah': do_blah_thing()
    else: do_non_blah_thing()
    
    try: something()
    finally: cleanup()
    
    do_one(); do_two(); do_three(long, argument,
                                 list, like, this)
    
    if foo == 'blah': one(); two(); three()

Line Length

  • Keep each line of code to a readable length. Unless you have a reason to, keep lines to fewer than 120 characters.[link]

    Long lines mean you are doing too much on a single line.

Line Breaks

  • The closing brace/bracket/parenthesis on multiline constructs should be lined up under the first character of the line that starts the multiline construct. [link]

    # Yes
    my_list = [
        'item 1',
        'item 2',
        'item 3',
        'itme 4',
        'item 5',
        'item 6',
    ]
    result = some_function_that_takes_arguments(
        'arg1',
        'arg2',
        'arg3',
        'arg4',
    )
    
    # No
    my_list = [
        'item 1',
        'item 2',
        'item 3']
    
    my_list = [
        'item 1',
        'item 2',
        'item 3'
        ]
  • Add line break before binary operators. [link]

    This applies to and and or as well

    # No: operators sit far away from their operands
    income = (gross_wages +
              taxable_interest +
              (dividends - qualified_dividends) -
              ira_deduction -
              student_loan_interest)
    
    # Yes: easy to match operators with operands
    income = (
        gross_wages
        + taxable_interest
        + (dividends - qualified_dividends)
        - ira_deduction
        - student_loan_interest
    )
  • Add line breaks when it improves readability. For instance, add line breaks between the mapping, looping, and (optional) conditional parts of a comprehension. [link]

    # No
    employee_hours = [schedule.earliest_hour for employee in self.employess for schedule in employee.schedules]
    return min(h for h in employee_hours if h is not None)
    
    # Yes
    employee_hours = [
        schedule.earliest_hour
        for employee in self.employess
        for schedule in employee.schedules
    ]
    return min(
        hour
        for hour in employee_hours
        if hour is not None
    )
  • For a very short comprehension, it is acceptable to use just one line of code.[link]

    sum_of_squares = sum(n**2 for n in numbers)

Trailing Commas

  • Trailing commas are usually optional, except they are mandatory when making a tuple of one element.[link]

    # Yes
    FILES = ('setup.cfg',)
    
    # OK, but confusing
    FILES = 'setup.cfg',
  • When trailing commas are redundant, they are often helpful when a version control system is used, when a list of values, arguments or imported items is expected to be extended over time. The pattern is to put each value (etc.) on a line by itself, always adding a trailing comma, and add the close parenthesis/bracket/brace on the next line. However it does not make sense to have a trailing comma on the same line as the closing delimiter (except in the above case of singleton tuples).[link]

    # Yes
    FILES = [
        'setup.cfg',
        'tox.ini',
    ]
    initialize(
        FILES,
        error=True,
    )
    
    # No
    FILES = ['setup.cfg', 'tox.ini',]
    initialize(FILES, error=True,)

Blank Lines

  • Surround top-level function and class definitions with two blank lines.[link]

  • Method definitions inside a class are surrounded by a single blank line.[link]

  • Use blank lines in functions, sparingly, to indicate logical sections.[link]

Comprehensions

  • Use comprehensions when possible over writing multiline for loops.[link]
    measurements = [1, 1, 2, 3, 4, 5]
    high_measurements = []
    
    # No
    for m in measurements:
        if m > 3:
            high_measurements.append(m)
    
    # Yes
    high_measurements2 = [
        m
        for m in measurements
        if m > 3
    ]
    
    # Yes, returns generator
    high_measurements_gen = (
        m
        for m in measurements
        if m > 3
    )
    
    # Yes, returns distinct elements (set)
    high_measurements_set_gen = {
        m
        for m in measurements
        if m > 3
    }
    
    # Yes, for dictionaries
    employees_count = {
        key: value
        for key, value in [('Berlin', 350), ('Zurich', 50)]
    }

Source File Encoding

  • Files using ASCII (in Python 2) or UTF-8 (in Python 3) should not have an encoding declaration.[link]

Imports

  • Imports should usually be on separate lines and in alphabetic order if they are too many.[link]

  • Imports are always put at the top of the file, just after any module comments and docstrings, and before module globals and constants. [link]

  • Imports should be grouped in the following order with a blank line between each group of imports: [link]

    1. standard library imports
    2. related third party imports
    3. local application/library specific imports
  • Wildcard imports (from <module> import *) should be avoided. [link]

String Quotes

  • Use single-quoted strings whenever possible. [link]

  • When a string contains single quote characters, use the double quote ones to avoid backslashes in the string. It improves readability.[link]

  • For triple-quoted strings, always use double quote characters to be consistent with the docstring convention in PEP 257.[link]

Commenting

Though a pain to write, comments are absolutely vital to keeping our code readable. The following rules describe what you should comment and where. But remember: while comments are very important, the best code is self-documenting. Giving sensible names to types and variables is much better than using obscure names that you must then explain through comments.

When writing your comments, write for your audience: the next contributor who will need to understand your code. Be generous — the next one may be you!

Google C++ Style Guide

  • Comments that contradict the code are worse than no comments. Always make a priority of keeping the comments up-to-date when the code changes! [link]

  • Comments should be complete sentences. The first word should be capitalized, unless it is an identifier that begins with a lower case letter (never alter the case of identifiers!).[link]

Inline Comments

An inline comment is a comment on the same line as a statement. Inline comments should be separated by at least two spaces from the statement. They should start with a # and a single space.

  • Use inline comments sparingly.[link]

  • Inline comments are unnecessary and in fact distracting if they state the obvious.[link]

    # Don't do this:
    x = x + 1                 # Increment x
    
    # But sometimes, this is useful:
    x = x + 1                 # Compensate for border

Documentation Strings

  • Write docstrings for all public modules, functions, classes, and methods.[link]

  • Docstrings are not necessary for non-public methods, but you should have a comment that describes what the method does. This comment should appear after the def line.[link]

  • Conventions for writing good documentation strings (a.k.a. "docstrings") are immortalized in PEP 257.[link]

TODO comments

  • Use TODO comments for code that is temporary, a short-term solution, or good-enough but not perfect.[link]

  • TODOs should include the string TODO in all caps, and optionally followed by the full name of the person who can best provide context about the problem referenced by the TODO, in parentheses.[link]

  • A colon is optional.[link]

  • A comment explaining what there is to do is required. The main purpose is to have a consistent TODO format that can be searched to find the person who can provide more details upon request.[link]

  • A TODO is not a commitment that the person referenced will fix the problem. Thus when you create a TODO, it is almost always your name that is given. [link]

      # No
      # TODO: check.
    
      # Bad
      # TODO(RS): Use proper namespacing for this constant.
    
      # Bad
      # TODO(drumm3rz4lyfe): Use proper namespacing for this constant.
    
      # Ok, but rather mention the full name
      # TODO: Use proper namespacing for this constant.
    
      # Good
      # TODO(Ringo Starr): Use proper namespacing for this constant.

TODO vs FIXME

  • Use # FIXME: to annotate problems.[link]

    def truncate(sentence):
        # FIXME: shouldn't use a global here.
        return sentence[:CUT]
  • Use # TODO: to annotate solutions to problems. [link]

    def truncate(sentence):
        # TODO: replace CUT with a function param.
        return sentence[:CUT]

Commented-out code

  • Never leave commented-out code in our codebase. [link]

Naming Conventions

  • Never use the characters 'l' (lowercase letter el), 'O' (uppercase letter oh), or 'I' (uppercase letter eye) as single character variable names. [link]

  • Modules (filenames) should have short, all-lowercase names. Underscores can be used in the module name if it improves readability.[link]

  • Python packages (directories) should also have short, all-lowercase names, although the use of underscores is discouraged. [link]

  • Class names should normally use the CapWords convention. [link]

    • The naming convention for functions may be used instead in cases where the interface is documented and used primarily as a callable.
  • Because exceptions should be classes, the class naming convention applies here. However, you should use the suffix "Error" on your exception names (if the exception actually is an error).[link]

  • Function names should be lowercase, with words separated by underscores as necessary to improve readability.[link]

  • Always use self for the first argument to instance methods.[link]

  • Always use cls for the first argument to class methods.[link]

  • If a function argument's name clashes with a reserved keyword, it is generally better to append a single trailing underscore rather than use an abbreviation or spelling corruption. Thus class_ is better than clss. (Perhaps better is to avoid such clashes by using a synonym.) [link]

  • Use one leading underscore only for non-public methods and instance variables.[link]

  • Constants are usually defined on a module level and written in all capital letters with underscores separating words. Examples include MAX_OVERFLOW and TOTAL. [link]

  • Public attributes should have no leading underscores. [link]

  • Do not shorten words or smash them together without a separating underscore.[link]

  • It is preferred to name functions with a verb. (Even if it means putting get_ or find_ in front of the function name) [link]

  • Use long variable names, whole words and multiple words. [link]

Strings

  • Use .format() in Python 2 and string literals in Python 3. [link]

    currency = 'USD'
    rank = 2
    rate = 0.3510
    
    # No
    message = 'Currency: %s, rank: %d, rate: %.2f' % (currency, rank, rate)
    
    # Ok
    message = 'Currency: {}, rank: {}, rate: {:.2f}'.format(currency, rank, rate)
    
    # Good in 2.x (Too verbose but allows easier migration to Python 3.6+)
    message = 'Currency: {currency}, rank: {rank}, rate: {rate:.2f}'.format(
        currency=currency,
        rank=rank,
        rate=rate,
    )
    
    # Yes in 3.x
    message = f'Currency: {currency}, rank: {rank}, rate: {rate:.2f}'
  • Join a list of values together using the join method. [link]

    animals = ['cat', 'dog', 'mouse']
    output = ', '.join(animals)

Regular Expressions

  • Avoid using regular expressions if there's a simpler and equally accurate way of expressing your target search/transformation. [link]
  • Unless your regular expression is extremely simple, always use a multi-line string and VERBOSE mode when representing your regular expression. [link]

Conditionals

  • Comparisons to singletons like None should always be done with is or is not, never the equality operators.[link]

Truthiness

  • Do not check emptiness (strings, lists, tuples) through length or other means. Use the fact that empty sequences are false.[link]

    # No
    if len(results) == 0:
        print('No results found.')
    
    if len(failures) > 0:
        print('There were failures during processing.')
    
    # Yes
    if not results:
        print('No results found.')
    
    if failures:
        print('There were failures during processing.')
  • Do not rely on truthiness for checking zeroness or non-zeroness though.[link]

    # No:
    if n % 2:
        print('The given number is odd')
    
    if not step_count:
        print('No steps taken.')
    # Yes:
    if n % 2 == 1:
        print('The given number is odd')
    
    if step_count == 0:
        print('No steps taken.')
  • Don't compare boolean values to True or False using ==. [link]

    # Yes
    if greeting:
    
    # No
    if greeting == True:
    
    # Worse
    if greeting is True:

Long if-elif chains

  • Python does not have switch statements. Instead, you'll often see Python developers use an if statement with many elif statements. Instead of using many elif statements, consider using a dictionary. This alternative is often (but not always) possible. [link]
    # No
    if word == 'zero':
        numbers.append(0)
    elif word == 'one':
        numbers.append(1)
    elif word == 'two':
        numbers.append(2)
    elif word == 'three':
        numbers.append(3)
    elif word == 'four':
        numbers.append(4)
    elif word == 'five':
        numbers.append(5)
    elif word == 'six':
        numbers.append(6)
    elif word == 'seven':
        numbers.append(7)
    elif word == 'eight':
        numbers.append(8)
    elif word == 'nine':
        numbers.append(9)
    else:
        numbers.append(' ')
    
    # Yes
    word_to_digit = {
        'zero': 0,
        'one': 1,
        'two': 2,
        'three': 3,
        'four': 4,
        'five': 5,
        'six': 6,
        'seven': 7,
        'eight': 8,
        'nine': 9,
    }
    numbers.append(word_to_digit.get(word, ' '))

Conversion to bool

  • If you ever see code that sets a variable to True or False based on a condition. Rely on truthiness by converting the condition to a bool instead, either explicitly for the truthy case or implicitly using not for the falsey case.

    # No
    if results:
        found_results = True
    else:
        found_results = False
    
    if not failures:
        success = True
    else:
        success = False
    
    # Yes
    found_results = bool(results)
    
    success = not failures
  • Keep in mind that sometimes no conversion is necessary. The condition here is already a boolean value. So type-casting to a bool would be redundant. Instead simply set the variable equal to the expression.

    # No
    if n % 2 == 1:
        is_odd = True
    else:
        is_odd = False
    
    # Yes
    is_odd = (n % 2 == 1)

Recommendations

Compacting Assignments

  • It is fine to use iterable unpacking to compact multiple assignment statements onto one line. Do this only when the assignments are very tightly related.

    word1, word2 = word1.upper(), word2.upper()
    x, y, z = (a1 - a2), (b1 - b2), (c1 - c2)

Naming Indexes

  • Whenever you see something like some_variable[0] or some_variable[2], treat this as an indication that you should be relying on iterable unpacking.
    # No
    do_something(things[0], things[1])
    
    do_something(things[0], things[1:-1], things[-1])
    
    # Yes
    first, second = things
    do_something(first, second)
    
    head, *middle, tail = things
    do_something(head, middle, tail)

Named Tuples

  • Whenever you see yourself using tuples and indexes to access them, define a namedtuple.
    # No
    
    employee = ('Guido', 'Berlin', 61)
    if employee[2] > 65:
        print(f'Sorry {employee[1]}, it is time to rest')
    
    # Yes
    from collections import namedtuple
    
    Employee = namedtuple('Employee', 'name city age')
    employee = Employee(name='Guido', city='Berlin', age=61)
    if employee.age > 65:
        print(f'Keep going {employee.name}!')

Exceptions

  • When catching exceptions, mention specific exceptions whenever possible instead of using a bare except: clause.[link]

  • For all try/except clauses, limit the try clause to the absolute minimum amount of code necessary. Again, this avoids masking bugs. [link]

    # Yes
    
    try:
        value = collection[key]
    except KeyError:
        return key_not_found(key)
    else:
        return handle_value(value)
    
    # No
    
    try:
        # Too broad!
        return handle_value(collection[key])
    except KeyError:
        # Will also catch KeyError raised by handle_value()
        return key_not_found(key)
  • Define your custom exceptions and explicitly catch them. [link]

Return statements

  • Be consistent in return statements. Either all return statements in a function should return an expression, or none of them should. If any return statement returns an expression, any return statements where no value is returned should explicitly state this as return None, and an explicit return statement should be present at the end of the function (if reachable).
    # No
    
    def foo(x):
        if x >= 0:
            return math.sqrt(x)
    
    def bar(x):
        if x < 0:
            return
        return math.sqrt(x)
    
    # Yes
    
    def foo(x):
        if x >= 0:
            return math.sqrt(x)
        else:
            return None
    
    def bar(x):
        if x < 0:
            return None
        return math.sqrt(x)

Loops

  • If you ever see range(len(colors)), consider whether you actually need an index. Never do this.[link]

    # No
    for i in range(len(colors)):
        print(colors[i])
  • Use zip instead of an index to loop over multiple lists at the same time. [link]

    # Yes
    for color, ratio in zip(colors, ratios):
        print('{}% {}'.format(ratio * 100, color))
  • If you do really need an index, use enumerate. [link]

    # Yes
    for number, name in enumerate(presidents, start=1):
        print('President {}: {}'.format(number, name))

Be Consistent

If you're editing code, take a few minutes to look at the code around you and determine its style. If they use spaces around all their arithmetic operators, you should too. If their comments have little boxes of hash marks around them, make your comments have little boxes of hash marks around them too.

The point of having style guidelines is to have a common vocabulary of coding so people can concentrate on what you're saying rather than on how you're saying it. We present global style rules here so people know the vocabulary, but local style is also important. If code you add to a file looks drastically different from the existing code around it, it throws readers out of their rhythm when they go to read it. Avoid this.

Google C++ Style Guide

A Foolish Consistency is the Hobgoblin of Little Minds

One of Guido's key insights is that code is read much more often than it is written. The guidelines provided here are intended to improve the readability of code and make it consistent across the wide spectrum of Python code. As PEP 20 says, "Readability counts".

A style guide is about consistency. Consistency with this style guide is important. Consistency within a project is more important. Consistency within one module or function is the most important.

However, know when to be inconsistent -- sometimes style guide recommendations just aren't applicable. When in doubt, use your best judgment. Look at other examples and decide what looks best. And don't hesitate to ask!

PEP 8 -- Style Guide for Python Code

In particular:

  • Do not break backwards compatibility just to comply with this guide!
  • Do not pep8-ify code for the sake of pep8-ify-ing it. This is meta-work and often brakes git blame usage.