- Target Python Version: 3.12
- Target Pydantic Version: 2.0
- Use Pydantic models as the primary data structure.
- Prefer composition over inheritance.
- Use Protocols to define interfaces instead of Abstract Base Classes (ABCs).
- Use type hints consistently throughout the code.
- Follow PEP 8 guidelines for naming conventions and code formatting.
- High Cohesion: Ensure each module or class has a single, well-defined responsibility.
- Low Coupling: Minimize dependencies between modules and classes using dependency injection and well-defined interfaces.
- Use composition to build complex objects from simpler ones.
- Avoid deep inheritance hierarchies.
- Use mixins sparingly.
- Define data structures first using Pydantic models.
- Use type hints to clearly specify data types.
- Design APIs around data structures, not behaviors.
- Use Protocols to define interfaces.
- Code against interfaces (Protocols) rather than concrete implementations.
- Apply dependency inversion to decouple modules.
- Implement factory patterns or use dependency injection for object creation.
- Pass dependencies as parameters; avoid creating them within methods.
- Use configuration objects for complex object creation.
- Prefer simple solutions over complex ones.
- Avoid unnecessary functionality (YAGNI principle).
- Refactor and simplify code regularly.
- Use built-in generic types:
list[int]
,dict[str, float]
,tuple[int, int]
. - Import types from
typing
when necessary:Callable
,TypeVar
,Generic
,Protocol
. - Use
|
for union types:int | str
. - Use
Type | None
for optional types. - Annotate all function parameters and return types.
- Handle forward references with postponed evaluation of annotations (default in Python 3.12).
Example:
def process_data(data: list[int | str]) -> dict[str, int]:
...
- Use
Protocol
fromtyping
to define interfaces. - Name Protocols clearly, optionally using a "Protocol" suffix.
- Define methods in Protocols without implementations.
- Use structural subtyping (duck typing).
- Use
@runtime_checkable
ifisinstance()
checks are needed.
Example:
from typing import Protocol, runtime_checkable
@runtime_checkable
class DataProcessor(Protocol):
def process(self, data: list[int]) -> list[int]:
...
- Use
UpperCamelCase
for enum class names, suffixed withEnum
. - Use
UPPER_SNAKE_CASE
for enum members. - Assign values using integers, strings, or
auto()
. - Use
==
operator for comparisons.
Example:
from enum import Enum, auto
class StatusEnum(Enum):
SUCCESS = auto()
FAILURE = auto()
if result.status == StatusEnum.SUCCESS:
...
- Inherit from
pydantic.BaseModel
. - Use type annotations for all fields.
- Use
Field()
for validation and metadata. - Use
@field_validator
for custom validation logic. - Use the
model_config
attribute for model-wide settings. - Use Pydantic's built-in types, like
EmailStr
. - Leverage Pydantic V2 features, such as
model_serializers
and improved validation.
Example:
from pydantic import BaseModel, Field, EmailStr, field_validator
class User(BaseModel):
id: int
name: str = Field(..., max_length=50)
email: EmailStr
@field_validator('name')
def name_must_be_capitalized(cls, value: str) -> str:
if not value.istitle():
raise ValueError('Name must be capitalized')
return value
model_config = {
'json_schema_extra': {
'examples': [
{
'id': 1,
'name': 'John Doe',
'email': '[email protected]'
}
]
}
}
- Compose models by using other models as fields.
- Create models for shared attributes.
- Use functions or services for shared behavior.
- Keep Pydantic and ORM models in sync, defining mappings between them.
Example:
class Address(BaseModel):
street: str
city: str
zip_code: str
class Customer(BaseModel):
id: int
name: str
address: Address
- Use Pydantic models for request and response schemas.
- Use FastAPI for API development.
- Design RESTful endpoints around resources.
- Use
async def
for I/O-bound endpoints. - Use
Depends
for dependency injection. - Validate inputs with Pydantic.
Example:
from fastapi import FastAPI, Depends
from pydantic import BaseModel
app = FastAPI()
class ProductCreate(BaseModel):
name: str
price: float
class ProductResponse(BaseModel):
id: int
name: str
price: float
@app.post("/products/", response_model=ProductResponse)
async def create_product(product: ProductCreate):
...
- Use SQLAlchemy for ORM.
- Create separate ORM and Pydantic models, keeping them in sync.
- Link models via composition.
- Use the repository pattern with Protocols.
- Use async drivers for database operations.
- Leverage Python 3.12 features for improved performance.
- Use Alembic for database schema migrations.
SQLAlchemy ORM Example:
# ORM model using SQLAlchemy
from sqlalchemy import Column, Integer, String, Float
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.asyncio import AsyncSession
Base = declarative_base()
class ProductORM(Base):
__tablename__ = 'products'
id = Column(Integer, primary_key=True, index=True)
name = Column(String(255), nullable=False)
price = Column(Float, nullable=False)
# Pydantic model
from pydantic import BaseModel
class Product(BaseModel):
id: int
name: str
price: float
@classmethod
def from_orm(cls, orm_obj: ProductORM) -> 'Product':
return cls(
id=orm_obj.id,
name=orm_obj.name,
price=orm_obj.price,
)
Using Alembic for Migrations:
- Initialize Alembic in your project using
alembic init alembic
. - Configure
alembic.ini
andenv.py
to connect to your database and import your models. - Generate migration scripts using
alembic revision --autogenerate -m "create products table"
. - Apply migrations using
alembic upgrade head
.
Alembic Configuration Example:
# alembic.ini
[alembic]
script_location = alembic
sqlalchemy.url = sqlite+aiosqlite:///./test.db
# alembic/env.py
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
import your_app.models # Import your ORM models here
config = context.config
fileConfig(config.config_file_name)
target_metadata = your_app.models.Base.metadata # Use your Base's metadata
def run_migrations_online():
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
run_migrations_online()
- Use dependency injection for classes and functions.
- Implement a container or use libraries like
dependency-injector
. - Define interfaces with Protocols.
- Use
Depends
in FastAPI endpoints.
Example:
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
DATABASE_URL = "sqlite+aiosqlite:///./test.db"
engine = create_async_engine(DATABASE_URL)
async_session = async_sessionmaker(engine, expire_on_commit=False)
async def get_db() -> AsyncSession:
async with async_session() as session:
yield session
@app.get("/products/")
async def read_products(db: AsyncSession = Depends(get_db)):
...
- Use
async def
for I/O-bound functions. - Use async libraries for database and network operations.
- Avoid blocking calls in async functions.
- Utilize
asyncio
improvements in Python 3.12, like Task Groups.
Example:
import asyncio
async def fetch_data():
...
async def process_data():
...
async def main():
async with asyncio.TaskGroup() as tg:
tg.create_task(fetch_data())
tg.create_task(process_data())
- Use pytest for tests.
- Mirror application structure in tests.
- Use fixtures for setup/teardown.
- Focus on meaningful tests.
- Use mocks to isolate tests.
- Test async code with
pytest-asyncio
. - Utilize new testing features in Python 3.12.
Example:
import pytest
import asyncio
@pytest.mark.asyncio
async def test_create_product():
...
- Validate all inputs with Pydantic.
- Sanitize outputs to prevent injections.
- Implement authentication and authorization.
- Securely handle sensitive data.
- Use type annotations to prevent type-related vulnerabilities.
- Use descriptive names for all identifiers.
- Adhere to the Single Responsibility Principle.
- Group related code logically.
- Use type aliases for complex types.
- Create functions for shared behavior.
- Write pure functions when possible.
- Avoid global state.
- Use immutable data structures.
- Leverage
functools
anditertools
for functional patterns.
- For complex behavior implementations.
- For performance-critical code.
- When integrating with external libraries that require inheritance.
from typing import Protocol, runtime_checkable
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
@runtime_checkable
class UserRepository(Protocol):
async def get_user(self, user_id: int) -> User:
...
async def create_user(self, user: User) -> User:
...
from pydantic import BaseModel
from datetime import datetime
class Timestamps(BaseModel):
created_at: datetime
updated_at: datetime
class Post(BaseModel):
id: int
title: str
content: str
timestamps: Timestamps
from typing import Protocol
from pydantic import BaseModel
class NotificationService(Protocol):
async def send_notification(self, message: str) -> None:
...
class AlertSystem:
def __init__(self, notifier: NotificationService):
self.notifier = notifier
async def alert(self, msg: str):
await self.notifier.send_notification(msg)
from fastapi import FastAPI, Depends
from pydantic import BaseModel
from typing import Protocol
app = FastAPI()
class MessageCreate(BaseModel):
content: str
class MessageResponse(BaseModel):
id: int
content: str
class MessageService(Protocol):
async def create_message(self, message: MessageCreate) -> MessageResponse:
...
async def get_message_service() -> MessageService:
...
@app.post("/messages/", response_model=MessageResponse)
async def create_message(
message: MessageCreate,
service: MessageService = Depends(get_message_service)
):
return await service.create_message(message)