TS-35: Python
This technical standard provides guidelines for writing Python code that is clear, maintainable, and consistent. It is based on PEP 8, the language’s own style guide, and Google’s Python Style Guide, supplemented with the typing-related PEPs and current community tooling conventions. Other sources are listed in the references section at the end of this document.
Terminology
Throughout this document, the following words shall have the following meanings:
- "Module" means a single
.pyfile. - "Package" means a directory of modules containing an
init.pyfile (a "regular package") or, since Python 3.3, one without (a "namespace package"). This standard’s use of "package" always means the import-time construct, never a distributable unit published to a package index — that meaning is written out in full as "distribution package" wherever it is intended. - "Dunder" means a name with a double underscore on both ends, eg.
init,repr. Short for "double underscore". - "Callable" means any object that can be invoked with
()— a function, a method, a class, or an instance implementingcall. - "Type hint" and "type annotation" are used interchangeably to mean the optional static-typing syntax introduced by PEP 484 and extended by later PEPs.
Source files
Source files are distinguished by the .py extension. Module names MUST
follow the naming rules in Naming conventions: all lower case, with
underscores if it improves readability, and MUST NOT collide with a standard
library module name (eg. do not name a module json.py or types.py).
Encoding and line endings
Source files MUST be encoded using UTF-8, with no byte order mark. Python 3
assumes UTF-8 source by default; an explicit # -- coding: utf-8 --
declaration is unnecessary and SHOULD NOT be added.
Source files MUST use Unix-style line endings (LF, \n), not Windows-style
(CRLF, \r\n).
Indentation MUST use 4 spaces per level. Tab characters MUST NOT be used for
indentation, and MUST NOT be mixed with spaces — Python 3 raises a
TabError if a file mixes the two inconsistently, but a mix that happens to
be consistent within one file is still a readability hazard for anyone whose
editor renders tabs at a different width.
Line length
Lines SHOULD be wrapped at 88 characters, the default enforced by Black and adopted by Google’s Python Style Guide. This is a deliberate departure from PEP 8’s stricter 79-character limit: 88 characters better matches modern wide-screen editors while still comfortably supporting two files side by side, and is now the more common convention in new Python codebases. A project MAY instead adopt PEP 8’s 79-character limit if it must interoperate closely with a codebase that already enforces it, but MUST NOT mix the two limits within a single project.
Docstrings and comments SHOULD be wrapped more conservatively, at 72 characters, matching PEP 8’s recommendation for prose.
Module layout
A module’s contents MUST be laid out in the following order, from top to bottom:
- Module docstring (see Docstrings).
from futureimports, if any.- Standard library imports.
- Third-party imports.
- Local application/library imports.
- Module-level "dunder" attributes (
all,version), if any. - Module-level constants.
- Classes and functions.
- A
if name == "main":guard, if the module is also runnable as a script.
Each of the three import groups (stdlib, third-party, local) MUST be
separated from the others by a single blank line, and the imports within each
group SHOULD be sorted alphabetically. isort, or an equivalent formatter,
SHOULD be used to enforce this automatically rather than maintained by hand.
"""Utilities for parsing and validating inbound webhook payloads.""" from __future__ import annotations import hashlib import json from typing import Any import requests from pydantic import BaseModel from myapp.errors import ValidationError from myapp.settings import get_settings MAX_PAYLOAD_BYTES = 1_048_576
Wildcard imports (from module import *) MUST NOT be used, except in the
init.py of a package that is deliberately re-exporting a curated public
API — and even then, the re-exported names SHOULD be listed explicitly in
all rather than left implicit.
Relative imports (from . import sibling, from ..pkg import thing) MAY be
used within a package, but MUST NOT reach more than one level up
(from …pkg import thing and deeper). A module that needs to reach further
up the package tree SHOULD use an absolute import instead.
Module size
A single module SHOULD NOT exceed roughly 1,000 lines. This is a soft
guideline, not a hard limit — the underlying signal is whether the module
still has one clear responsibility, and a module can earn its length by
covering one cohesive area. Once a module accumulates unrelated
responsibilities, it SHOULD be split along those responsibility boundaries,
usually into a package (a directory with an init.py) containing
several smaller modules.
Naming conventions
Spelling
All file names, code, comments, and docstrings MUST be written in English, with American English preferred for spelling.
Modules and packages
Modules MUST be named using lower_snake_case, eg. payment_gateway.py. Short,
all-lowercase names with no underscore are also acceptable where the name is a
single word, eg. errors.py.
Packages (directories containing an init.py) MUST be named using all
lower case, and SHOULD avoid underscores where a single concatenated word is
readable, eg. webapi, matching the convention recommended by
PEP 8. This differs from module naming,
where underscores are preferred for multi-word names — the distinction
mirrors PEP 8’s own guidance that package names should stay short.
Classes and exceptions
Classes MUST be named using UpperCamelCase, eg. PaymentGateway,
UserRepository.
Class names are typically nouns or noun phrases. They SHOULD be descriptive and unambiguous, and SHOULD NOT be overly long.
Exception classes MUST be named using UpperCamelCase and MUST end with the
suffix Error, eg. ValidationError, PaymentDeclinedError — not
Exception, which is too generic to be useful in a except SomeException:
clause, and not the bare noun without a suffix, which reads ambiguously
against non-exception classes of the same name.
Functions, methods, and variables
Functions, methods, and variables (including local variables and instance
attributes) MUST be named using lower_snake_case, eg. send_message,
compute_total, user_count.
Function and method names SHOULD, typically, be verbs or verb phrases. Variable names SHOULD, typically, be nouns or noun phrases.
A single leading underscore (_internal_helper) MUST be used to signal that a
name is internal to a module or class and not part of its public interface.
This is a convention only — Python does not enforce it — but it MUST be
honored by callers: code outside the module or class MUST NOT reference a
single-underscore name directly.
A double leading underscore with no trailing underscore (__really_private)
invokes Python’s name-mangling within a class body. This SHOULD be reserved
for the narrow case where a subclass accidentally overriding an attribute
would break the base class — it is a mechanism for avoiding name collisions
in an inheritance hierarchy, not a stronger form of "private", and MUST NOT
be used as the default way to mark internal attributes (use a single
underscore for that).
A single trailing underscore (class_, type_) MAY be used to avoid
colliding with a Python keyword or a built-in name, in preference to
misspelling the identifier (klass) or shadowing the built-in.
Dunder names (init, repr, eq) are reserved for the
interpreter’s own protocol methods and MUST NOT be invented for
application-level names — defining a new dunder to mean something the
language does not already give it a meaning for is confusing to any reader
who has learned the standard set.
One- or two-character names SHOULD be reserved for small scopes with an
obvious, conventional meaning: loop counters (i, j), coordinates
(x, y), or a comprehension’s throwaway variable (_). They SHOULD NOT be
used for anything with a lifetime longer than a few lines or a scope wider
than a single function body.
Constants
Module-level constants MUST be named using UPPER_SNAKE_CASE, eg.
MAX_RETRIES, DEFAULT_TIMEOUT_SECONDS.
As in TS-33: Java's definition, a "constant" here means a
module-level value that is set once, at import time, and never reassigned.
Python has no const keyword, so this is a naming convention only, signaling
intent to the reader rather than an enforced guarantee.
MAX_RETRIES = 3
DEFAULT_TIMEOUT_SECONDS = 30.0
SUPPORTED_CURRENCIES = frozenset({"USD", "EUR", "GBP"})Type variable names
Type variables, declared with typing.TypeVar or the class Foo[T] /
def foo[T] generic syntax (Python 3.12+), SHOULD be named with a single
capital letter, optionally followed by a digit, eg. T, KT, VT,
following the convention established by the typing module itself.
Code style
Automated formatting
Code formatting — indentation, blank lines, line wrapping, quote-character choice, trailing commas — MUST be delegated to an autoformatter, run in CI and as a pre-commit hook, rather than maintained by hand or enforced through code review. Black or Ruff’s formatter (which is Black-compatible) are RECOMMENDED. This standard’s line-length and blank-line rules describe what such a formatter already does by default; they are stated here so the standard is self-contained, not as an instruction to apply them manually.
A project MUST pick one formatter and apply it uniformly. Reformatting the
entire codebase in one commit when adopting a formatter, isolated from any
functional change, is RECOMMENDED, so that git blame is not muddied by
mixing the reformat with unrelated diffs.
Blank lines
Two blank lines MUST separate top-level function and class definitions. One blank line MUST separate method definitions within a class. A single blank line MAY be used sparingly within a function body to group related statements.
import os
class PaymentGateway:
def __init__(self, api_key: str) -> None:
self.api_key = api_key
def charge(self, amount: int) -> None:
...
def main() -> None:
...Whitespace
A single space MUST surround binary operators (x = a + b, not x=a+b), with
the exception of keyword arguments and default parameter values in a function
signature, which MUST NOT be surrounded by spaces when unannotated
(def f(x=1):), but MUST be surrounded by spaces when the parameter carries a
type annotation (def f(x: int = 1):) — following PEP 8’s rule that the =
here is read as part of the annotated declaration, not a bare assignment.
No space MUST appear immediately inside parentheses, brackets, or braces
(spam(ham[1], {eggs: 2}), not spam( ham[ 1 ], { eggs: 2 } )), and no space
MUST appear before a comma, semicolon, or colon, but a space MUST follow one
(except at the end of a line).
Trailing commas
A trailing comma SHOULD be included after the last element of a multi-line list, tuple, dict, set, or function call/signature. This keeps the diff to a single line when a further element is added later, and is what Black inserts automatically in "magic trailing comma" mode.
SUPPORTED_REGIONS = [
"us-east-1",
"eu-west-1",
"ap-southeast-2",
]A single-line collection or call MUST NOT carry a trailing comma before the
closing bracket, except the one-element tuple, where the trailing comma is
mandatory syntax ((value,)), not a style choice.
String quotes
Single and double quotes are equally acceptable for string literals; PEP 8 takes no position, and this standard does not either — a project SHOULD adopt whichever its autoformatter enforces (Black defaults to double quotes) and apply it consistently, rather than mixing both by hand. Whichever is chosen, use the other quote character to avoid backslash-escaping a quote that appears inside the string, per PEP 8.
Triple double-quotes (""") MUST be used for docstrings, regardless of which
quote style is chosen for ordinary string literals — see Docstrings.
f-strings (f"{value}") SHOULD be preferred over %-formatting or
.format() for constructing strings that interpolate values, for readability
and because they are evaluated at the point of use rather than requiring
positional or keyword argument matching.
Comparisons
Singletons (None, True, False) MUST be compared with is / is not,
never == / !=.
# ✓
if value is None:
...
# ✗
if value == None:
...Emptiness of a sequence, string, or collection SHOULD be tested with an
implicit boolean check (if not items:), not an explicit length comparison
(if len(items) == 0:), unless the distinction between "empty" and "falsy for
another reason" genuinely matters for the value in question — for example, a
value that could be either None or 0, where collapsing both to "falsy"
would be a bug.
Type checks SHOULD use isinstance(), not a direct comparison of type(),
because isinstance() respects subclassing.
# ✓
if isinstance(value, int):
...
# ✗
if type(value) == int:
...Programming constructs
EAFP over LBYL
Python idiom generally prefers "Easier to Ask Forgiveness than Permission" (EAFP) — attempt the operation and handle the exception — over "Look Before You Leap" (LBYL) — check preconditions before attempting the operation. EAFP SHOULD be preferred where the failure case is a routine, expected outcome (a missing dictionary key, a file that may not exist, a type that may not support an operation), because it avoids a race condition between the check and the use, and because the CPython interpreter is optimized for the successful path.
# ✓ EAFP
try:
value = mapping[key]
except KeyError:
value = default
# ✗ LBYL — has a race, and reads worse
if key in mapping:
value = mapping[key]
else:
value = defaultLBYL MAY still be preferred where the check is cheap, the failure path would be expensive or side-effecting to trigger deliberately, or where an explicit guard clause reads more clearly than a broad exception handler.
Comprehensions and generator expressions
A list, dict, or set comprehension SHOULD be preferred over an equivalent
for loop with .append() when the body is a single, simple expression.
# ✓
squares = [n * n for n in range(10)]
# ✗ — same result, more ceremony
squares = []
for n in range(10):
squares.append(n * n)A comprehension SHOULD be abandoned in favor of an explicit for loop once
it needs a second for clause beyond simple flattening, a non-trivial
if condition, or any side effect — a comprehension with a side effect
(calling a function for its effect rather than its return value) is a code
smell, since the construct exists to build a collection, not to execute
statements.
A generator expression ((x for x in iterable), no brackets) SHOULD be
preferred over a list comprehension when the result will only be iterated
once and does not need to be indexed, reused, or measured with len() — it
avoids materializing the whole collection in memory.
Context managers
Anything that acquires a resource requiring explicit release — a file handle,
a network connection, a lock, a database transaction — MUST be managed with a
with statement, not a manual acquire()/release() or open()/close()
pair. The with statement guarantees the resource is released even if the
body raises.
# ✓
with open(path, encoding="utf-8") as f:
contents = f.read()
# ✗ — leaks the handle if read() raises
f = open(path, encoding="utf-8")
contents = f.read()
f.close()A class that manages its own resource SHOULD implement the context manager
protocol (enter/exit) rather than exposing separate
open/close-style methods, so its callers get the same with-statement
guarantee. contextlib.contextmanager SHOULD be used to implement a simple
context manager as a generator function, in preference to hand-writing a
class with enter/exit when no other state needs to live on the
object.
Mutable default arguments
A mutable object (a list, dict, or set) MUST NOT be used as a default argument value. Default argument values are evaluated once, at function definition time, not on each call — so a mutable default is silently shared and mutated across every call that relies on it, which is very rarely the intended behavior.
# ✗ — the same list is reused and grows across calls
def add_item(item, items=[]):
items.append(item)
return items
# ✓
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return itemsUnpacking
Tuple/iterable unpacking SHOULD be used in preference to indexed access when assigning several related values at once, including the starred form for "the rest of the values".
first, *middle, last = values
Swapping two variables SHOULD use tuple unpacking (a, b = b, a), not a
temporary variable.
Loops
enumerate() SHOULD be used instead of manually tracking an index with
range(len(…)).
# ✓
for index, item in enumerate(items):
...
# ✗
for index in range(len(items)):
item = items[index]
...zip() SHOULD be used to iterate over two or more sequences in lockstep,
instead of indexing each by a shared counter.
A for/else or while/else construct MAY be used where its semantics
(the else block runs only if the loop completed without break) genuinely
fit the logic, but authors SHOULD add a short comment noting that the else
is loop-else, not the more commonly expected if-else — this construct
is unfamiliar enough that an unmarked use is a readability tax on the reader.
Error handling
Exception handlers MUST catch the narrowest exception type that is actually
expected, never a bare except: and, except at a genuine top-level boundary
(a request handler, a task runner) that must not crash regardless of cause,
not a bare except Exception: either.
Custom exceptions SHOULD be defined for a library or module’s own error
conditions, subclassing the most specific applicable built-in exception (or
Exception directly if none fits), so callers can distinguish this code’s
failures from unrelated ones.
Where an exception is re-raised after partial handling — logging it, adding
context, or translating it to a different type — raise … from err SHOULD
be used to preserve the original traceback as the new exception’s
cause, rather than losing it or, worse, swallowing it with a bare
raise NewError() that discards where the failure actually originated.
try:
parse(payload)
except ValueError as err:
raise ValidationError("invalid payload") from errTypes and typing
Type hints
Every public function, method, and module-level variable MUST carry type hints for its parameters and return value, per PEP 484 and PEP 526. Private (single-underscore) helpers SHOULD also be annotated, but the requirement is stricter for public interfaces because they are the contract other code depends on.
def calculate_total(items: list[Item], tax_rate: float) -> Decimal:
...The builtin generic syntax (list[Item], dict[str, int]) MUST be used
instead of the deprecated typing.List, typing.Dict, etc., which are
retained only for backward compatibility with Python versions predating
PEP 585 (3.9). A project supporting an
older Python version MUST use the typing equivalents, or
from future import annotations to defer evaluation and use the
builtin syntax anyway — see Deferred evaluation of annotations.
A value that may be absent MUST be annotated as X | None (Python 3.10+, per
PEP 604) or the equivalent
Optional[X] on earlier versions — never left as a bare X with an implicit
None default silently tolerated by the caller. A function returning X |
None signals to every caller that the None case MUST be handled.
Any SHOULD be avoided. It disables type checking for the value it is
applied to and for everything derived from it, so its use SHOULD be
confined to genuine boundary cases — deserializing untrusted external data
before it has been validated into a concrete shape, or interoperating with an
untyped third-party library — and MUST NOT be used merely to silence a type
checker error that reflects a real type mismatch.
Static type checking
A static type checker — mypy or pyright — SHOULD run in CI against the whole codebase, not merely on a subset of files, and its failures SHOULD block the build. Where a codebase is being migrated to typed code incrementally, the checker MAY be configured to only enforce strict checking on already-annotated modules, with a plan to widen that scope over time, rather than left permanently partial.
Type-checking suppressions (# type: ignore) MUST carry the specific error
code being suppressed (# type: ignore[assignment]) and a brief comment
explaining why, so the suppression doesn’t silently swallow an unrelated
future error in the same line.
Deferred evaluation of annotations
from future import annotations (per
PEP 563) SHOULD be added at the top of a
module that uses type hints, so that annotations are stored as strings and
evaluated lazily rather than at function-definition time. This allows the
newer X | None union syntax and builtin generic syntax to be used even on
Python versions that would otherwise require typing.Optional and
typing.List, and avoids a class needing to forward-reference its own name
in a method signature.
Structured data
A dataclass (@dataclasses.dataclass) SHOULD be used, rather than a bare
dict, tuple, or hand-written class with a manual init, whenever a piece
of code needs a record type with named fields.
from dataclasses import dataclass
@dataclass(frozen=True)
class Money:
amount: int
currency: strfrozen=True SHOULD be set unless the dataclass genuinely needs to be
mutated after construction — an immutable record is easier to reason about
and safe to share across threads.
typing.NamedTuple MAY be preferred over a dataclass where tuple semantics
(unpacking, positional access, hashability by default) are actually wanted;
a plain, untyped tuple or collections.namedtuple SHOULD NOT be used for a
new record type where either of the typed alternatives is available.
A TypedDict SHOULD be used, rather than an untyped dict[str, Any], to
describe the shape of dictionary data with a fixed, known set of string keys
— most commonly, JSON payloads deserialized from an external API.
Protocols (typing.Protocol) SHOULD be used to define a structural interface
— "anything with this method signature" — in preference to an abstract base
class, when the point is to accept any object with the right shape rather
than to share implementation via inheritance.
Documentation and comments
Docstrings
Every public module, class, function, and method MUST have a docstring, per PEP 257. A private (single-underscore) function SHOULD have one if its behavior is not obvious from its name and signature alone.
Docstrings MUST use triple double-quotes ("""), even for a one-line
docstring, for consistency and because triple-quoting is required as soon as
the docstring spans multiple lines.
A one-line docstring MUST fit on a single line, phrased as a command
("Return the total.", not "Returns the total." or "This function returns the
total."), and the closing """ MUST sit on the same line as the opening one.
def is_valid(value: str) -> bool:
"""Return True if value is a syntactically valid account ID."""
...A multi-line docstring MUST have a one-line summary on the first line,
followed by a blank line, followed by further detail. The closing """ MUST
sit on its own line.
This standard adopts
Google-style
docstring sections — Args:, Returns:, Raises: — over the alternative
reStructuredText (:param:, Sphinx-native) or NumPy styles, because they
read cleanly as plain text even before any documentation generator processes
them.
def charge(account_id: str, amount: Decimal) -> Receipt:
"""Charge the given account and return a receipt.
Args:
account_id: The account to charge, in "acct_<id>" form.
amount: The amount to charge, in the account's home currency.
Returns:
A Receipt recording the transaction ID and settled amount.
Raises:
InsufficientFundsError: If the account balance is too low.
AccountNotFoundError: If account_id does not exist.
"""
...A class’s docstring MUST describe the class’s purpose and, where it carries
public attributes not already documented via dataclass field types or
property docstrings, MUST document them under an Attributes: section
following the same style as Args:.
A module’s docstring — the first statement in the file, before any import — MUST summarize what the module contains, in enough detail that a reader can decide whether to open it without doing so.
Comments
Comments MUST explain why the code does something non-obvious, not what the code does when that is already clear from reading it. A comment that merely restates the following line in English adds noise, not information.
# ✓ — explains a non-obvious constraint retries = 3 # the upstream gateway rate-limits after 3 attempts/second # ✗ — restates the code retries = 3 # set retries to 3
Inline comments (on the same line as code) SHOULD be used sparingly, separated from the code by at least two spaces, and SHOULD be reserved for a short clarification that doesn’t warrant its own line.
Block comments SHOULD be complete sentences, with the first word capitalized (unless it is an identifier that begins with a lowercase letter), and SHOULD be kept up to date — a comment that describes behavior the code no longer has is worse than no comment, since it actively misleads the next reader.
# TODO(username): description SHOULD be used to mark unfinished work,
following the convention recommended by
Google’s
Python Style Guide, with the username identifying who to ask for context —
not necessarily who will do the work.
# TODO(kieran): remove once the legacy v1 endpoint is retired.
Commented-out code MUST NOT be committed. Version control already preserves history; a block of dead code left in a comment only accumulates and rots.
Project structure and tooling
Project metadata
Every Python project MUST declare its metadata and dependencies in
pyproject.toml, per
PEP 621 — not the legacy setup.py /
setup.cfg combination, which SHOULD only be retained where a build backend
genuinely requires it (eg. a package with a compiled extension using a build
backend that has not yet adopted PEP 621).
A lock file — generated by whichever dependency manager the project uses (eg.
uv.lock, poetry.lock) — MUST be committed alongside pyproject.toml for
any application (as opposed to a library intended for reuse under a range of
dependency versions), so that every environment installs identical dependency
versions.
Virtual environments
Dependencies MUST be installed into an isolated virtual environment, never
into the system-wide interpreter. uv,
Poetry, or the standard library’s venv module
are all acceptable; uv is RECOMMENDED for new projects for its speed and
because it unifies environment creation, dependency resolution, and locking
in one tool.
A committed .python-version file (or the equivalent pin in
pyproject.toml) SHOULD specify the exact interpreter version a project
targets, so that contributors and CI resolve to the same version rather than
whatever happens to be on PATH.
Package layout
A distributable library SHOULD use the "src layout" — application code under
src/<package_name>/, with tests in a sibling top-level tests/ directory —
rather than a "flat layout" with the package at the repository root. The src
layout prevents the package from being accidentally importable from the
repository root without being installed, which catches packaging mistakes
(a missing entry in the package manifest) that a flat layout would hide
during local development.
project/
├── pyproject.toml
├── uv.lock
├── src/
│ └── myapp/
│ ├── __init__.py
│ └── ...
└── tests/
└── ...A standalone application that is deployed rather than distributed as an installable package (eg. a service deployed as a container image) MAY use a flat layout instead, since the packaging-mistake risk the src layout guards against does not apply the same way.
Linting, formatting, and type checking
Ruff is RECOMMENDED as the project’s linter,
covering the majority of what flake8, isort, pyupgrade, and several
other single-purpose tools historically covered separately, and MAY also be
used as the project’s formatter (see Automated formatting) in place of
Black. Where both Ruff’s formatter and Black are viable, either is
acceptable; a project MUST NOT run both, since they can disagree on edge
cases.
Static type checking (see Static type checking) SHOULD run as a separate step from linting — mypy or pyright are not a substitute for Ruff, nor Ruff for them, since Ruff performs no type inference.
Linting, formatting (in check mode), and type checking SHOULD each run as a required CI check, and SHOULD also be available locally as pre-commit hooks so that violations are caught before a commit is pushed, not only in CI.
Testing
pytest is RECOMMENDED over the standard library’s
unittest for new test suites, for its plain assert-based assertions,
fixture system, and parametrization support. A project already built on
unittest MAY continue using it — pytest can run unittest-style test cases
unmodified — rather than being forced through a disruptive rewrite.
Test files MUST be named test_<module>.py or <module>_test.py
(consistently, whichever a project chooses) so that pytest’s default
discovery picks them up without additional configuration.
References
- Van Rossum, G, Warsaw, B, and Coghlan, N (2001). PEP 8: Style Guide for Python Code. — The language’s own official style guide, and the primary source for this standard’s formatting, whitespace, and naming conventions.
- Goodger, D and Van Rossum, G (2001). PEP 257: Docstring Conventions. — The source for this standard’s docstring structure and placement rules.
- Google.
Google Python Style
Guide. — The source for the 88-character line length,
TODOcomment format, and Google-style docstring sections used in this standard. - Van Rossum, G, Lehtosalo, J, and Langa, Ł (2014). PEP 484: Type Hints. — Introduces the type hint syntax this standard requires on public interfaces.
- Levkivskyi, I, Roach, S, Van Rossum, G, and Lehtosalo, J (2017). PEP 526: Syntax for Variable Annotations. — Extends type hints to variable declarations.
- Solomatin, E (2019).
PEP 585: Type Hinting Generics In
Standard Collections. — The source for preferring builtin generic syntax
(
list[int]) over the deprecatedtypingaliases. - Cannon, B (2018).
PEP 563: Postponed Evaluation of
Annotations. — The source for the
from future import annotationsrecommendation in Deferred evaluation of annotations. - Moss, A and Elliott, C (2019).
PEP 604: Allow writing union types as
X | Y. — The source for the
X | Noneunion syntax preferred in Type hints. - Python Software Foundation. The Python Tutorial. — The official introductory reference for the language.
- Astral. Ruff. — The linter and formatter recommended in formatting, and type checking.
- Astral. uv. — The package and environment manager recommended in Virtual environments.
- Python Software Foundation. src layout vs flat layout. — The source for the package layout guidance in Package layout.