Python Best Practice
Christian Külker
2020-01-18
Python-Best-Practice
Table Of Contents

This guides follows some ideas from The Hitchhiker's Guide To Python and other ideas from the internet and myself. It is a project in progress.

PEP (Python Enhanced Proposals)

IDE

This sections contains only IDE and editors that I have Python experience with. Be aware that this section is incomplete.

Vim

Make vim PEP8 compatible. Best if vim was compiled with +python.

set textwidth=79  " lines longer than 79 columns will be broken
set shiftwidth=4  " operation >> indents 4 columns; << unindents 4 columns
set tabstop=4     " a hard TAB displays as 4 columns
set expandtab     " insert spaces when hitting TABs
set softtabstop=4 " insert/delete 4 spaces when hitting a TAB/BACKSPACE
set shiftround    " round indent to multiple of 'shiftwidth'
set autoindent    " align the new line indent with the previous line
set statusline+=%#warningmsg#
set statusline+=%{SyntasticStatuslineFlag()}
set statusline+=%*
let g:syntastic_auto_loc_list=1
let g:syntastic_loc_list_height=5

Tools

Pip

Install pip if not already done.

IPython

IPhthon should be used interactively with the features: shell, web-base notebook, data data visualization, GUI toolkits, parallel execution.

pip install ipython      # base install only
pip install ipython[all] # notebook qtconsole, tests, ...

Environment

Use virtual environments on a per project base (also with pip).

Bash: (for pip install, this will make pip require a virtual environment)

export PIP_REQUIRE_VIRTUALENV=true

See this example environment. TODO (/srv/env/python)

Project Layout

This is the layout of the project sample

README.rst
LICENSE             # full license text
Makfile             # not mandatory
setup.py            # package distribution management
requirements.txt    # pip requirements file
sample/__init__.py  # 'sample' is the project name, mostly empty
sample/core.py      # 'sample' is the project name
sample/helpers.py   # 'sample' is the project name
docs/conf.py        # package reference documentation
docs/index.rst      # package reference documentation
tests/test_basic.py
tests/test_advanced.py

Makefile

init:
    pip install -r requirements.txt
test:
    py.test tests
.PHONY:init test
clean:
    # remove bytecode
    find . -type f -name "*.py[co]" -delete -or -type d -name "__pycache__" -delete

Testing

import os
import sys
sys.path.insert(0,os.path.abspath(os.path.join(os.path.dirname(__file__),'..')))

import sample
from.context import sample

Modules

Import

"""Bad:"""
        from MODULE import *
"""
        FUNCTION(PARAMETER)
"""

"""Better:"""
        from MODULE import FUNCTION
"""
        FUNCTION(PARAMETER)
"""

"""Best:"""
        import MODULE
"""
        MODULE.FUNCTION(PARAMETER)
"""

Packages

The statement import pack.module will look for the file pack/__init.py__ and execute all top level statements and than looks for pack/module.py and execute all top level statements.

Function

Use stateless functions, if possible, because pure functions are:

Decorators

Context Managers

Dynamic Typing

Mutable (Changeable) and Immutable (Fixed) Types

"""Bad"""
nums = ""
for n in range(20):
    nums += str(n) # slow and inefficient
print nums

"""Good"""
nums = []
for n in range(20):
nums.append(str(n))
print "".join(nums) # much more efficient

"""Better"""
nums = [str(n) for n in range(20)]
print "".join(nums)

"""Best"""
nums = map(str, range(20))
print "".join(nums)
foo = 'foo'
bar = 'bar'
foobar = foo + bar # This is good
foo += 'ooo' # This is bad, instead you should do:
foo = ''.join([foo, 'ooo'])
foo = 'foo'
bar = 'bar'
foobar = '%s%s' % (foo, bar) # It is OK
foobar = '{0}{1}'.format(foo, bar) # It is better
foobar = '{foo}{bar}'.format(foo=foo, bar=bar) # It is best

Explicit Code

"""Bad"""
def make_complex( *args):
    x, y = args
    return dict( **locals())
"""Good"""
def make_complex(x, y):
    return {'x': x, 'y': y}

Statement Per Line

Function Parameters (Arguments)

Positional Arguments

   print_at(x,y)
   draw_line(x1,y1,x2,y2)

Keyword Arguments

    send(message, to, cc=None, bcc=None)

Arbitrary Argument List

"""Bad"""
send(message, *args)
send('Hello', ['Bilbo', 'Frodo', 'Sauron']
"""Better"""
send(message, recipients)
send('Hello', ['Bilbo', 'Frodo', 'Sauron']

Arbitrary Keyword Argument Dictionary

Magic

Be a nice user

Return values

Documentation

While there usually one way to do it. It seems when it comes to documentation there are more.

Python makes no exception by suggesting to maintain a README file for project documentation and less a INSTALL document as ways of installing are usually known and similar. The Python Guide suggests the presence of a LICENSE file.

If the project gets bigger and the README will get to long (only then) parts of the README might be offloaded to a TODO file and a CHANGELOG file.

For the format reStructuredText or Markdown is suggested.

Usually this is sufficient for small and medium sized projects. Often the dead of an aspiring open source software project arise when the project reaches the size, that a README is not sufficient any more. Then a wiki or even a home page is suggested and political factions emerge that sometimes fighting severe bike shed color wars. The following short paragraph is for these situations.

Shpinx is a popular Python documentation tool utilize reStructuredText and spits HTML and PDF out.

For other parts it is advisable to already document the source code. Python can be documented well with docstrings (PEP 0257#specification) in favor of block comments (PEP 8#comments).

A docstring comment is basically a multi line comment with three quotes and in some cases they can be used to supplement unit tests.

def add_two_numbers(x, y):
    """
    SUM = add_two_numbers(NUMBER_0, NUMBER_1)

    - Combine two NUMBERS via the operation of addition
    - Require a two NUMBERS: NUMBER_0, NUMBER_1
    - Return the sum of 2 NUMBERS
    """
    return x + y

Of course in such an obvious case a one line docstring comment is apropriate

def add_two_numbers(x, y):
    """Add two numbers and return the sum."""
    return x + y

Bytecode

Git

.gitignore

*.pyc
*.pyo
*.pyd
__pycache__

Or

  # Will match .pyc, .pyo and .pyd files.
*.py[cod]
  # Exclude the whole folder
__pycache__/

License

Package

Documentation

The Hitchhiker’s Guide To Python

History

Version Date Notes
0.1.1 2022-06-09 Shell->bash, +history, documtation
0.1.0 2020-01-18 Initial release