General style#
You may also refer to common guidance like the Google Python Style Guide.
See also
The Services section contains Python-related content for PostgreSQL and RabbitMQ.
Naming#
There are only two hard things in computer science: cache invalidation, naming things and off-by-one errors.
Use
lower_snake_case
for everything except constants (UPPER_SNAKE_CASE
) and classes (UpperCamelCase
).Use the same terminology as other projects. At minimum, don’t use the same term for a different concept.
Use terminology from Enterprise Integration Patterns.
Don’t use “cute” names.
Type hints#
Type hints are especially useful in packages for documentation using Sphinx and linting using Mypy. Use of type hints is optional.
Note
Since Mypy has many open issues for relatively common scenarios, using Mypy to validate your type hints is optional.
Reference: typing – Support for type hints
Exception handling#
Do not use a bare
except:
or a genericexcept Exception:
. Use specific error classes to avoid handling exceptions incorrectly.Do not catch an exception and raise a new exception, unless the new exception has a special meaning (e.g.
CommandError
in Django).If an unexpected error occurs within a long-running worker, allow the worker to die. For example, if a worker is failing due to a broken connection, it should not survive to uselessly attempt to re-use that broken connection.
String formatting#
Tip
Don’t use regular expressions or string methods to parse and construct filenames and URLs.
Use the pathlib (or os.path) module to parse or construct filenames, for cross-platform support.
Use the urllib.parse module to parse and construct URLs, notably: urlsplit (not urlparse
), parse_qs, urljoin and urlencode. To replace part of a URL parsed with the urlsplit
function, use its _replace method. See examples.
See also
How to construct SQL statements
Format strings (f-strings), introduced in Python 3.6 via PEP 498, are preferred for interpolation of variables:
message = f"hello {name}"
For interpolation of expressions, the str.format() method is preferred if it is easier to read and write. For example:
message = "Is '{name}' correct?".format(name=person["name"])
or:
message = "Is '{person[name]}' correct?".format(person=person)
is easier to write than:
message = f"""Is '{person["name"]}' correct?""" # AVOID
There are two cases in which f-strings and str.format()
are not preferred:
- Logging
“Formatting of message arguments is deferred until it cannot be avoided.” If you write:
logger.debug("hello {}".format("world")) # WRONG
then
str.format()
is called whether or not the message is logged. Instead, please write:logger.debug("hello %s", "world")
- Internationalization (i18n)
String extraction in most projects is done by the
xgettext
command, which doesn’t support f-strings. To have a single syntax for translated strings, use named placeholders and the%
operator, as recommended by Django. For example:_('Today is %(month)s %(day)s.') % {'month': m, 'day': d}
Remember to put the
%
operator outside, not inside, the_()
call:_('Today is %(month)s %(day)s.' % {'month': m, 'day': d}) # WRONG
Note
To learn how to use or migrate between %
and format()
, see pyformat.info.
Maintenance#
Maintainers can find improper formatting with these regular expressions. Test directories and Sphinx conf.py
files can be ignored, if needed.
Unnamed placeholders, except for log messages,
strftime()
, psycopg2.extras.execute_values() and common false positives (e.g.%
inSECRET_KEY
default value):(?<!info)(?<!debug|error)(?<!getenv)(?<!warning)(?<!critical|strftime)(?<!exception)(?<!execute_values)\((\n( *['"#].*)?)* *['"].*?%[^( ]
Named placeholders, except for translation strings and SQL statements:
(?<!\b[t_])(?<!one|all)(?<!pluck)(?<!gettext|execute|sql\.SQL)\((\n( *['"#].*)?)* *['"].*?%\(
Named placeholders, with incorrect position of
%
operator (trailing space):%\(.+(?<!\) )%
Log messages using f-strings or
str.format()
(case-sensitive), ignoring the extra keyword argument, ArgumentParser.error and Directive.error:^( *)(?:\S.*)?\b(?<!self\.)(?<!subparser\.)_?(?:debug|info|warning|error|critical|exception)\((?:\n(\1 .+)?)*.*?(?<!extra=){
Translation strings using f-strings or
str.format()
:^( *)(?:\S.*)?(?:\b__?|gettext|lazy)\((?:\n(\1 .+)?)*.*?(?<!% ){
Remaining occurrences of
str.format()
:[^\w\]]\.format\(
To correct any remaining occurrences of str.format()
, use these patterns and replacements:
Pattern |
Replacement |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Long strings#
For cases in which whitespace has no effect, like SQL statements, use multi-line strings:
cursor.execute("""
SELECT *
FROM table
WHERE id > 1000
""")
For cases in which whitespace changes the output, like log messages, use consecutive strings:
logger.info(
"A line with up to 119 characters. Use consecutive strings, one on each line, without `+` operators or join "
"methods. Do not start a string with a space. Instead, append it to the previous string. If the message has "
"multiple sentences, do not break the line at punctuation."
)
However, in some cases, it might be easier to edit in the form:
from textwrap import dedent
content = dedent("""\
# Heading
A long paragraph.
- Item 1
- Item 2
- Item 3
""")
Maintainers can find improper use of multi-line strings with this regular expression:
(?<!all|raw)(?<!dedent)(?<!execute)\((\n( *)(#.*)?)*"""
Default values#
Use dict.setdefault
instead of a simple if-statement. A simple if-statement has no elif
or else
branches, and a single statement in the if
branch.
data.setdefault('key', 1)
if 'key' not in data: # AVOID
data['key'] = 1
Maintainers can find simple if-statements with this regular expression:
^( *)if (.+) not in (.+):(?: *#.*)?\n(?: *#.*\n)* +\3\[\2\] = .+\n(?!(?: *#.*\n)*\1(else\b|elif\b| \S))
Input/Output#
import sys
print('message', file=sys.stderr)
sys.stderr.write('message\n') # WRONG
See also
Functional style#
itertools
, filter()
and map()
can be harder to read, less familiar, and longer. On PyPy, they can also be slower.
Instead of using filter()
and map()
with a lambda expression, you can use a list comprehension in most cases. For example:
output = list(filter(lambda x: x < 10, xs)) # AVOID
output = [x for x in xs if x < 10]
output = list(map(lambda x: f'a strong with {x}', xs)) # AVOID
output = [f'a string with {x}' for x in xs]
That said, it is fine to do:
output = map(str, xs)
Object-oriented style#
Don’t force polymorphism and inheritance, especially if it sacrifices performance, maintainability or readability.
Python provides encapsulation via modules. As such, functions are preferred to classes where appropriate.
See also
Maintainers can find class hierarchies, excluding those imposed by dependencies (Click, Docutils, Django, Django REST Framework, and standard libraries), with this regular expression:
\bclass \S+\((?!(AdminConfig|AppConfig|Directive|Exception|SimpleTestCase|TestCase|TransactionTestCase|json\.JSONEncoder|yaml.SafeDumper)\b|(admin|ast|click|forms|migrations|models|nodes|serializers|template|views|viewsets)\.|\S+(Command|Error|Warning)\b)
Standard library#
Use @dataclass for simple classes only. Using
@dataclass
with inheritance, mixins, class variables, etc. tends to increase complexity.
Scripts#
If a repository requires a command-line tool for management tasks, create an executable script named manage.py
in the root of the repository. (This matches Django.)
If you are having trouble with the Python path, try running the script with python -m script_module
, which will add the current directory to sys.path
.
Examples: extension_registry, deploy
See also
Comments#
Use sentence case, correct punctuation, and correct capitalization. Do not omit articles.
Do not add
TODO
comments. Instead, create GitHub issues. TODO’s are less visible to the team.Maintainers can find TODO comments with this command: