A modified version of excellent original checklist by Paul Wolf
General
- Code is blackened with
black -
ruffhas been run with no errors -
mypyhas been run with no errors - Function complexity problems have been resolved using the default complexity index of
flake8. - Important core code can be loaded in iPython, ipdb easily.
- There is no dead code
- Comprehensions or generator expressions are used in place of for loops where appropriate
- Comprehensions and generator expressions produce state but they do not have side effects within the expression.
- Use
zip(),any(),all(),filter(), etc. instead of for loops where appropriate - Functions that take as parameters and mutate mutable variables don’t return these variables. They return None.
- Return immutable copies of mutable types instead of mutating the instances themselves when mutable types are passed as parameters with the intention of returning a mutated version of that variable.
- Avoid method cascading on objects with methods that return
self. - Function and method parameters never use empty collection or sequence instances like list
[]or dict{}. Instead they must useNoneto indicate missing input - Variables in a function body are initialised with empty sequences or collections by callables,
list(),dict(), instead of[],{}, etc. - Always use the
Finaltype hint for class instance parameters that will not change. - Context-dependent variables are not unnecessarily passed between functions or methods
- View functions either implement the business rules the view is repsonsible for or it passes data downstream to have this done by services and receives non-context dependent data back.
- View functions don’t pass
requestto called functions - Functions including class methods don’t have too many local parameters or instance variables. Especially a class’
__init__()should not have too many parameters. - Profiling code is minimal
- Logging is the minimum required for production use
- There are no home-brewed solutions for things that already exist in the PSL (python standard library)
n00b habbits
- Bare
exceptclause, Python uses exceptions to flag system level interupts such as sigkills. Don’t do this. - Argument default mutatable arguments such as
def foo(bar=[])are defined when the function is defined, not when its run, and will result in a all function calls sharing the same instance ofbar - Checking for equality using
==. Due to inheritance this is not desirable as it pins to a concrete type and not potentially it descendents. In other words the Liskov substitution principle. Insteadisinstance(p, tuple) - Explicit bool or length checks, such as
if bool(x)orif len(x) > 0is redundant, as Python has sane truthy evaluation. - Use of
rangeover thefor inidiom - If you really need the index, always use
enumerate - Not using items() on a dict
for k, v in dict.items()) - Using
time.timeto measure code performance. Usetime.perf_counterinstead. - Using
printstatements overlogging - Using
import *will normally liter the namespace with variable. Dont be lazy, be specific.from itertools import count
Imports and modules
- Imports are sorted by
isortor according to some standard that is consistent within the team - Import packages or modules to qualify the use of functions or classes so that unqualified function calls can be assumed to be to functions in the current module
Documentation
- Modules have docstrings
- Classes have docstrings unless their purpose is immediately obvious
- Methods and functions have docstrings
- Comments and docstrings add non-obvious and helpful information that is not already present in the naming of functions and variables
General Complexity
- Functions as complex as they need to be but no more (as defined by
flake8\ ’s default complexity threshold) - Classes have only as many methods as required and have a simple hierarchy
Context Freedom
- All important functionality can be loaded easily in
ipythonwithout having to construct dummy requests, etc. - All important functionality can be loaded in pdb (or a variant, ipdb, etc.)
Types
- Use immutable types ()
tuple,frozenset,Enum, etc) over mutable types whenever possible
Functions
- Functions are pure wherever possible, i.e. they take input and provide a return value with no side-effects or reliance on hidden state.
Modules
- Module level variables do not take context-dependent values like connection clients to remote systems unless the client is used immediately for another module level variable and not used again
Classes
- Every class has a single well-defined purpose. That is, the class does not mix up different tasks, like remote state acquisition, web sockets notification, data formatting, etc.
- Classes manage state and do not just represent the encapsulation of behaviour
- All methods access either
clsorselfin the body. If a method does not accessclsorself, it should be a function at module level. -
@classmethodis used in preference to@staticmethodbut only if the method body accessesclsotherwise the method should be a module level function. - Constants are declared at module level not in methods or class level
- Constants are always upper case
- Abstract classes are derived from abc:
from abc import ABC - Abstract methods use the
@abstractmethoddecorator - Abstract class properties use both
@abstractmethodand@propertydecorators - Classes do not use multiple inheritance
- Classes do not use mixins (use composition instead) except in rare cases
- Class names do not use the word “Base” to signal they are the single ancestor, like “BaseWhatever”
- Decorators are not used to replace classes as a design pattern
-
__init__()does not define too many local variables. Use the Parameter Consolidation pattern instead. - A factory class or function at module level is used for complex class construction (see Design Patterns) to achieve composition
- Classes are not dynamically created from strings except where forward reference requires this
Design Patterns
- Do not use designs that cause a typical Python developer to have to learn new semantics that are unexpected in Python
- Classes primarily use composition in preference to inheritance
- Beyond a very small number of simple variables, a class’ purpose is to acquire state for another class or it uses another class to acquire state in particular if the state is from a remote service.
- If you use the Context Parameter pattern, it is critical that the state of the context does not change after calling its
__init__(), i.e. it should be immutable - If a class’ purpose is to represent an external integration, you probably want numerous classes to compose the service:
RemoteDataClient,DomainManager,ContextManager,Factory,NotificationController,DomainResponse,DataFormatterand so on.