Rules for writing Python with the same spirit as strict TypeScript: explicit shapes, safe boundaries, and easy-to-debug data flow.
Don't: use loose strings scattered across the codebase for fixed values.
Do: use StrEnum for named string values that cross boundaries, or a Final mapping when you mostly need lookup/display values.
from enum import StrEnum
from typing import Final
class PaymentStatus(StrEnum):
PENDING = "pending"
PAID = "paid"
FAILED = "failed"
PAYMENT_STATUS_LABELS: Final[dict[PaymentStatus, str]] = {
PaymentStatus.PENDING: "Pending",
PaymentStatus.PAID: "Paid",
PaymentStatus.FAILED: "Failed",
}Why: Python cannot derive literal unions as ergonomically as TypeScript, so prefer a runtime value (StrEnum) that is still type-checkable and readable.
Do: use @dataclass(frozen=True, slots=True) for internal plain data objects when values are already trusted.
Do: use Pydantic at boundaries (API input/output, config, external payloads) where runtime validation and parsing matter.
Don't: pass untyped dict values deep into business logic.
from dataclasses import dataclass
from pydantic import BaseModel
class CreateUserInput(BaseModel):
email: str
name: str
@dataclass(frozen=True, slots=True)
class UserInfo:
id: int
email: str
name: strWhy: dataclasses keep internal code lightweight; Pydantic protects messy edges without turning every internal value into a validation model.
Do: prefer one typed input object for functions with multiple fields, and return a typed object instead of a tuple.
Don't: pass many positional arguments or return positional multi-values that callers must remember by order.
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class PriceInput:
amount: int
tax_rate: float
@dataclass(frozen=True, slots=True)
class PriceResult:
subtotal: int
tax: int
def calculate_price(input: PriceInput) -> PriceResult:
tax = round(input.amount * input.tax_rate)
return PriceResult(subtotal=input.amount, tax=tax)Why: this mirrors the POJO habit from TypeScript: named fields, safer refactors, and easier destructuring-like access.
- Do not use
Anyunless there is no practical alternative; useobject,Protocol, generics, or a validated boundary model. - Use
TypedDictonly for dictionary-shaped data you cannot reasonably model as a dataclass/Pydantic model. - Prefer
Protocolfor “anything with this behaviour” instead of inheritance-heavy abstractions. - Use
Finalfor constants that should not be reassigned.
- Fixed values crossing boundaries ->
StrEnum. - Internal POJO ->
@dataclass(frozen=True, slots=True). - Boundary validation/config/external payloads -> Pydantic
BaseModel. - Avoid deep untyped
dict; convert to typed objects near the edge. - Prefer one typed input object and one typed return object over many positional arguments/tuples.
- Avoid
Any; reach forProtocol, generics,TypedDict, or Pydantic validation.