PEP 8's naming rules are good enough. We follow them and add a small number of project conventions.
from typing import Final, Protocol
MAX_RETRIES: Final[int] = 3 # 2.1, 2.8 — module constant, annotated
_DEFAULT_TIMEOUT: Final[float] = 5.0 # 2.2 — internal, single leading underscore
class BookingReader(Protocol): # 2.1, 2.9 — Protocol named for the contract, no `I` prefix
def get_booking(self, booking_id: BookingId) -> Booking: ...
class BookingClient: # 2.5, 2.11 — owns the call surface, `Client` suffix
def get_booking(self, booking_id: BookingId) -> Booking: ... # 2.12 — raises if missing
def list_bookings(self) -> ItemPaged[Booking]: ... # 2.12 — pageable iterator
def create_booking(self, request: BookingRequest) -> Booking: ...
def booking_exists(self, booking_id: BookingId) -> bool: ... # 2.4, 2.12 — bool, never raises on absence
def should_retry(self, attempt: int) -> bool: # 2.4 — boolean reads as English
return attempt < MAX_RETRIES
def load_booking(client: BookingClient, booking_id: BookingId) -> Booking: # 2.5 — verb-first action
return client.get_booking(booking_id)Names carry their meaning without a type prefix: snake_case functions and variables against a PascalCase BookingClient (2.1), the Client suffix marking the call surface (2.11), and the get_/list_/create_/_exists verbs reading off the resource taxonomy (2.12). Booleans state a yes/no question (2.4), the constant is module-level and annotated (2.8), and the Protocol is named for its contract rather than carrying an I prefix (2.9).
Reasoning, step by step:
- Functions, methods, variables, parameters, module names:
snake_case. - Classes (and class-shaped things —
NamedTuple,Enum,TypedDict,Protocol):PascalCase. - Module-level constants:
SCREAMING_SNAKE_CASE. - Type variables:
T,K,Vfor canonical use;TItem,TKey,TValue(PascalCase withTprefix) when you need descriptive names. PEP 484 allows both. - Acronyms in class names are treated as words:
HttpClient,XmlParser. NotHTTPClient(that's the stdlib's choice forurllib; new code in our style usesHttpClient).
Enforcement: Ruff N801/N802/N803/N806 (pep8-naming) flag class, function, argument, and variable casing.
2.2 — Underscore prefixes signal visibility intent. Two leading underscores trigger name-mangling — use sparingly.
Reasoning, step by step:
_name— convention: "internal, don't import from outside the module." Not enforced by the language.__name(two leading, no trailing) — triggers name-mangling in classes (obj._Class__name). Use only when you're inheriting and need to avoid collisions; rare.__name__(two leading and trailing) — reserved for dunders. Don't invent your own.- Rule:
_for internal.__only when name-mangling is genuinely needed (subclassing scenarios). Never invent new dunders.
Enforcement: review; name-mangling __ and invented dunders are caught in read-through.
Reasoning, step by step:
- PEP 8: lowercase, optional underscores.
payment_client.py, notpaymentClient.pyorPaymentClient.py. - Short and singular.
user.pynotusers.py,payment.pynotpayments.py— unless the module genuinely covers the collection. - No Python keywords or stdlib collisions.
email.pynext to the stdlibemailpackage will hurt.
Enforcement: Ruff N999 (invalid module name) plus A005 (stdlib-shadowing module).
Reasoning, step by step:
is_active,has_default_card,should_retryreads as English.not activereads as "this isn't active."not is_activereads as "not is active" — slightly more awkward, but clearer about the negation.- Negative-form names compound badly.
not_ready→not not_readyis a double-negative head-scratcher.
Enforcement: review; boolean prefix and negative-form names are a read-through check.
Reasoning, step by step:
- Functions: verb-first.
parse_iso_date(),load_user(),close(). - Classes: noun.
UserRepository,PaymentRequest,JsonParser. - Properties / class attributes: noun or
is_/has_boolean.user.email,request.is_authenticated. - Beware:
*Manager,*Helper,*Util,*Handler— these often signal a class with no single responsibility. Consider a top-level function or a split into smaller classes.
Enforcement: review; verb/noun fit and *Manager/*Helper smells are caught in read-through.
Reasoning, step by step:
for i in range(n):is fine — the scope is one line.for u in users: load(u)is borderline — the scope is small butucarries no domain meaning.for user in users:is the safe default. Three extra letters cost nothing.- Exception:
x,y,zfor coordinates;i,j,kfor indices;nfor counts;_for "ignored." These are the canonical short names.
Enforcement: review; single-character names outside tight scope are a read-through check.
Reasoning, step by step:
test_returns_404_when_user_does_not_existis what you want to see in a test report.test_user_404reads like a code, not a sentence.- Use
snake_case(Python convention) — backticks for spaces aren't a Python feature. - Test names appear in CI output and flakiness dashboards. Treat them as public API of the test suite.
Enforcement: review; test-name descriptiveness is a read-through check.
Reasoning, step by step:
- Module-level constants get a type annotation:
MAX_RETRIES: Final[int] = 3. typing.Finaldocuments and enforces that the binding doesn't get reassigned (mypy checks).SCREAMING_SNAKE_CASEmakes constant-vs-variable visible at the call site.- Anti-pattern: writing
MAX_RETRIES = 3inside a function. That's not a constant — it's a re-evaluated local. Move to module scope, or just use the literal.
Enforcement: mypy enforces Final (no reassignment); Ruff N816 flags mixed-case module-level names.
Reasoning, step by step:
str_name,b_is_active,i_countare non-Pythonic. Type hints make this redundant.IUser(Java/C# interface prefix) is wrong for Protocol classes — useUserfor the Protocol if it's the canonical contract.- Acceptable affixes:
is_/has_for booleans (2.4),on_*for callbacks. - Don't suffix classes with
Async(BookingClientAsyncis wrong). Sync and async client classes share the class name and differ by module path (acme.booking.BookingClientvsacme.booking.aio.BookingClient). See 9.13. - A function-level
_asyncsuffix is acceptable only when a sync and async function coexist in the same module and the module isn't large enough to split. Prefer splitting.
Enforcement: review; Hungarian/type prefixes and Async class suffixes are caught in read-through.
Reasoning, step by step:
T = TypeVar("T")for generic "any type."K = TypeVar("K"),V = TypeVar("V")for key/value.T_co = TypeVar("T_co", covariant=True),T_contra = TypeVar("T_contra", contravariant=True)— variance is in the name.- For complex generic signatures, descriptive names help:
TUser = TypeVar("TUser", bound=User). PEP 695'stypestatement anddef foo[T](...)syntax (Python 3.12+) reduce the boilerplate.
Enforcement: review; type-variable naming and variance suffixes are a read-through check.
Reasoning, step by step:
- A class that owns connections, credentials, retry policies, and the call surface to an external service is a client. Name it that way:
PaymentClient,BookingClient,IndexClient. - Not
PaymentProxy,PaymentManager,PaymentService,PaymentAPI. The suffix isClientand onlyClient. Consistency lets a reader find the entry point by completion in 1–2 keystrokes. - Specialized sub-clients (when a service has nested resources) follow the same rule. A
paymentsattribute on aBookingClientreturns aPaymentClient, not aPaymentSubClientorPayments. - From Azure SDK guidelines: "DO name service client types with a
Clientsuffix."
Enforcement: review; the Client suffix on service-client classes is a read-through check.
Reasoning, step by step:
-
When methods operate on resources (CRUD-ish APIs, REST clients, repositories), pick verbs from a known taxonomy. Reader instantly knows the semantics; the whole API surface behaves consistently.
-
Verb → semantics:
Verb Semantics get_<noun>Fetch a resource. Raises if missing. Returns the entity. list_<noun>Enumerate resources. Returns a pageable iterator (never None, never a raw list of "all").create_<noun>Create a new resource. Raises if it already exists. upsert_<noun>Create or update. Idempotent. update_<noun>Modify an existing resource. Raises if missing. replace_<noun>Full replacement (PUT semantics). delete_<noun>Remove a resource. Succeeds (no-ops) even if missing. <noun>_existsReturns bool. Does NOT raise on "not found" — that's a normal response. Raises only on network/server errors.begin_<noun>Long-running operation. Returns a poller. See 10.17. append_<noun>Append to a collection. -
Rule: don't invent new verbs when one of these fits. Don't reuse a verb against its documented semantics —
delete_user(id)must not raise on missing. -
Anti-pattern:
fetch_user,read_user,find_user— pickget_userand be consistent. Synonyms read like a poorly-organized API. -
Cross-language consistency: when an organization ships SDKs in multiple languages for the same service, align the verbs across languages —
GetUser(Go),getUser(Kotlin),get_user(Python) all mean the same thing. The taxonomy here is the Python form.
Enforcement: review; verb-against-taxonomy and consistent semantics are caught in read-through.
# good
from typing import Final, Protocol
MAX_RETRIES: Final[int] = 3
_DEFAULT_TIMEOUT: Final[float] = 5.0 # module-private
class UserReader(Protocol):
def find(self, user_id: UserId) -> User | None: ...
def load_user(reader: UserReader, user_id: UserId) -> User:
user = reader.find(user_id)
if user is None:
raise UserNotFound(user_id)
return user
class BookingClient:
def get_booking(self, booking_id: BookingId) -> Booking: ... # 2.12 — raises if missing
def list_bookings(self, *, customer_id: CustomerId | None = None) -> ItemPaged[Booking]: ...
def create_booking(self, request: BookingRequest) -> Booking: ... # raises if exists
def delete_booking(self, booking_id: BookingId) -> None: ... # no-ops if missing
def booking_exists(self, booking_id: BookingId) -> bool: ... # returns bool
# bad
maxRetries = 3 # 2.1 — should be MAX_RETRIES
class IUser: # 2.5 / 2.9 — drop the prefix
pass
def loadUserById(id): # 2.1 — should be snake_case; `id` shadows builtin
pass
class BookingClientAsync: ... # 2.9 / 9.13 — use acme.booking.aio.BookingClient
class BookingManager: ... # 2.11 — should be BookingClient
def fetch_booking(): ... # 2.12 — should be get_bookingProtocoland structural typing: chapter 06.Finaland module constants: chapter 04.- Test naming: chapter 11.
- Client constructor and method shapes that pair with this verb taxonomy: chapter 10.