Explicit Python Conventions¶
Goals¶
- Make call contracts and package boundaries discoverable.
- Keep naming predictable and external work bounded.
Layout¶
Use a package entry point only when all child modules are private:
billing/
├── __init__.py # Re-export the supported API.
├── _quotes.py # Keep behavior and related errors together.
└── _transport.py # Bound external I/O and timeouts here.
If child modules are public, import them by explicit paths instead of building
a second API in __init__.py.
Guidance¶
- Declare expected parameters explicitly. Reserve
*argsand**kwargsfor forwarding or compatibility requirements. - Prefer keyword-only parameters when positional meaning is unclear.
- Return an explicit result only when it represents a normal domain outcome. Represent failures with specific exceptions instead of silently skipping unsuccessful work.
- Use singular nouns for classes and enums. Name values by domain role.
- Distinguish public and private members. Prefix private modules, models,
classes, methods, and attributes with
_. - Keep the public surface small and give each exported object one canonical import path. List re-exports explicitly and avoid wildcard imports.
- Use a trailing
_only for a genuine name collision. - Use type annotations for types, docstrings for public contracts, and comments for non-obvious implementation reasons.
- Store simple values as attributes. Use
@propertyonly for values computed on access. - Validate user-controlled input with Pydantic at the interface boundary. Do not duplicate that validation in trusted internal models.
- Set explicit timeouts for external calls.
- Prefer immutable values when callers do not need mutation. Follow the project's established record type.
Example¶
from collections.abc import Mapping
from dataclasses import dataclass
from decimal import Decimal
@dataclass(frozen=True)
class QuoteRequest:
product_code: str
units: Decimal
class QuoteUnavailable(Exception):
"""Raised when a quote cannot be calculated."""
def calculate_quote(
*,
request: QuoteRequest,
unit_prices: Mapping[str, Decimal],
) -> Decimal:
try:
unit_price = unit_prices[request.product_code]
except KeyError as exc:
raise QuoteUnavailable(request.product_code) from exc
return request.units * unit_price
The example keeps input immutable, dependencies explicit, the return type stable, and the handled exception specific.
Review Questions¶
- Can callers understand inputs and failure modes from the signature?
- Does every public object have one obvious import path?
- Can an external dependency block forever?