Python conventions¶
Modules¶
Add all public members to __all__
, declared immediately after the imports. (Note that mkdocstrings requires this.)
Use mainpkg/__init__.py
to import
the most important classes. Do not set __author__
or similar fields, but do set mainpkg/__version__
.
Formatting¶
Black or the Ruff formatter should be used, so don’t worry much about formatting. Avoid add trailing commas so that Black can decide whether to keep code on one line or to chop it.
Sometimes Black wraps lines in awkwardly, by prioritizing argument lists over call chains. If this happens, either shorten the lines or break the code into multiple statements. For example:
data = my_long_named_function_that_makes_the_line_too_long(
data
).my_other_long_named_function_being_chained(1)
data = my_shorter_function(data).my_other_shorter_function(1)
data = my_long_named_function_that_makes_the_line_too_long(data)
data = data.my_other_long_named_function_being_chained(1)
Classes¶
Use pydantic or dataclasses. Most libraries should use dataclasses only to avoid a dependency on pydantic. Use immutable types unless there’s a compelling reason otherwise.
Example
from typing import Self
from dataclasses import dataclass
@dataclass(slots=True, frozen=True, order=True)
class Cat:
breed: str | None
age: int
names: frozenset[str]
from pydantic import BaseModel
class Cat(BaseModel):
breed: str | None
age: int
names: frozenset[str]
class Config:
frozen = True
import orjson
from pydantic import BaseModel
def to_json(v) -> str:
return orjson.dumps(v).decode(encoding="utf-8")
def from_json(v: str):
return orjson.loads(v).encode(encoding="utf-8")
class Cat(BaseModel):
breed: str | None
age: int
names: frozenset[str]
class Config:
frozen = True
json_loads = from_json
json_dumps = to_json
Class members¶
Use @abstractmethod
in favor of @staticmethod
; @staticmethod
should only be used in the rare cases where @abstractmethod
cannot be used. As a general rule, prefer using a regular method over an @abstractmethod
.
Sort class members in the following order.
ClassVar
- attributes
@staticmethod
@classmethod
- magic methods
@property
methods, getters, and setters- regular methods
- inner classes
Within each of the 8 types, order by, in order of decreasing importance:
- Pairing getters and setters together, with the getter first.
- Listing public, then private (
_xxx
), then dunder (__xxx
).
OS compatibility¶
Use pathlib
instead of os
wherever possible. Always read and write text as UTF-8, and pass encoding="utf-8"
(i.e. not utf8
or UTF-8
).
Example
from pathlib import Path
directory = Path.cwd()
(directory / "myfile.txt").write_text("hi", encoding="utf-8")
Typing¶
Rationale
- Documentation generators such as mkdocstrings (for mkdocs) can use type annotations to provide helpful hints for users; type annotations also aid reading source code.
- Linters, IDEs, and other tools use them to detect mistakes.
- Tools can use type annotations to detect incorrect types at runtime. This can be especially useful because duck typing prevents complete test coverage.
- For annotating
self
andcls
: they are still subject to Ruff’s ANN rules.
Use typing annotations for both public APIs and internal components. Annotate all module-level variables, class attributes, and functions. Annotate both return types and parameters. Annotate self
, cls
, *args
, and **kwargs
parameters.
Example
from dataclasses import dataclass
from typing import Any, Self, Unpack
@dataclass(slots=True, frozen=True)
class A(SomeAbstractType):
value: int
@classmethod
def new_zero(cls: type[Self]) -> Self:
return cls(0)
def __add__(self: Self, other: Self) -> Self:
return self.__class__(self.value + other.value)
def add_sum(self: Self, *args: int) -> Self:
return self.__class(self.value + sum(args))
def delegate(self: Self, *args: Any, **kwargs: Unpack[tuple[str, Any]]) -> None:
... # first do something special
super().delegate(*args, **kwargs)
Docstrings¶
Use Google-style docstrings as mkdocstrings supports.
Ruff rules¶
Use Ruff to catch potential problems and bad practices. Use at least the rules enabled in the cicd pyproject.toml.
To disable counting a line or block in test coverage, use # nocov
(not # pragma: nocov
, etc.).