Python PR Checklist
A modified version of excellent original checklist by Paul Wolf
General⌗
- Code is blackened with
black
-
ruff
has been run with no errors -
mypy
has 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 useNone
to 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
Final
type 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
request
to 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
except
clause, 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) > 0
is redundant, as Python has sane truthy evaluation. - Use of
range
over thefor in
idiom - If you really need the index, always use
enumerate
- Not using items() on a dict
for k, v in dict.items()
) - Using
time.time
to measure code performance. Usetime.perf_counter
instead. - Using
print
statements 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
isort
or 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
ipython
without 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
cls
orself
in the body. If a method does not accesscls
orself
, it should be a function at module level. -
@classmethod
is used in preference to@staticmethod
but only if the method body accessescls
otherwise 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
@abstractmethod
decorator - Abstract class properties use both
@abstractmethod
and@property
decorators - 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
,DataFormatter
and so on.