Code Conventions¶
Import patterns, async conventions, schema patterns, model conventions, and API documentation standards.
Import Conventions¶
Relative Imports (Same App)¶
from .models import Loan
from . import selectors, services
from .providers import payment_processor_registry
Absolute Imports (Cross-App)¶
from apps.amortization.services import generate_schedule
from apps.borrowers.models import Borrower
from common.permissions import IsLoanOfficerOrAbove
Import sorting is enforced by ruff ("I" rule / isort):
Section Order¶
- Future imports
- Standard library
- Third-party packages
- First-party (
apps,common,config) - Local (relative imports)
Async Patterns¶
All Layers Are Async¶
# Controller (api.py)
@http_get("")
async def list_loans(self, ...):
return await selectors.loan_list()
# Service (services.py)
async def create_loan(**kwargs) -> Loan:
loan = await Loan.objects.acreate(**kwargs)
return loan
# Selector (selectors.py)
async def loan_list() -> QuerySet[Loan]:
return Loan.objects.filter(is_archived=False)
Django Async ORM¶
Use async variants for all ORM operations:
| Sync | Async |
|---|---|
Model.objects.create() |
Model.objects.acreate() |
Model.objects.get() |
Model.objects.aget() |
instance.save() |
instance.asave() |
instance.delete() |
instance.adelete() |
qs.count() |
qs.acount() |
qs.exists() |
qs.aexists() |
Transactions¶
Use sync with transaction.atomic(): — it works in async functions:
async def create_loan(**kwargs) -> Loan:
with transaction.atomic():
loan = await Loan.objects.acreate(**kwargs)
await generate_schedule(loan)
return loan
Warning
Do not use async with transaction.atomic(): — Django's Atomic class lacks __aenter__/__aexit__. Do not use the @transaction.atomic decorator on async functions.
sync_to_async¶
Only needed for third-party libraries without async support (e.g., django-guardian):
from asgiref.sync import sync_to_async
has_perm = await sync_to_async(user.has_perm)("view_loan", loan)
Model Conventions¶
Base Mixins¶
All tenant models inherit from mixins in common/models.py:
| Mixin | Fields Provided |
|---|---|
UUIDPrimaryKey |
id: UUID (auto UUIDv4) |
Timestamped |
created_at, updated_at (auto-set) |
ExternalID |
external_id: str (nullable, unique per tenant) |
Archivable |
is_archived: bool, archived_at: datetime |
Do not add these fields to individual models — they come from the mixins.
Field Types¶
| Data | Field Type |
|---|---|
| Money | MoneyField (django-money) — never DecimalField |
| Phone | PhoneNumberField (django-phonenumber-field) |
| Rates/percentages | DecimalField stored as decimal (0.065 not 6.5) |
| Extensible data | JSONBField |
| Enums | TextChoices (string-based, not integer) |
| FKs | UUID fields referencing other models |
| Attachable relations | GenericForeignKey + GenericRelation |
ClassVar for Meta Attributes¶
class Loan(BaseModel):
ordering: ClassVar[list[str]] = ["-created_at"]
indexes: ClassVar[list[models.Index]] = [
models.Index(fields=["status", "created_at"]),
]
Translation Markers on Models¶
from django.utils.translation import gettext_lazy as _
class Loan(BaseModel):
class Meta:
verbose_name = _("loan") # Lowercase
verbose_name_plural = _("loans")
status = models.CharField(
help_text=_("Current loan status."), # gettext_lazy for help_text
)
Pydantic Schema Conventions¶
Base Class¶
All schemas inherit from ninja.Schema which sets from_attributes = True. Do not add a Config class:
# CORRECT
class LoanOut(Schema):
loan_number: str = Field(description="Unique loan identifier")
# WRONG — triggers deprecation warning
class LoanOut(Schema):
class Config:
from_attributes = True # Already set by ninja.Schema
Field Documentation¶
Every field must have a description:
class LoanIn(Schema):
loan_number: str = Field(description="Unique loan identifier")
amount: PositiveDecimal = Field(description="Loan principal amount")
term_months: int = Field(description="Loan term in months")
Custom Serialization¶
Use @staticmethod def resolve_<field>() for custom serialization:
class BorrowerOut(Schema):
full_name: str
@staticmethod
def resolve_full_name(obj: Any) -> str:
return f"{obj.first_name} {obj.last_name}"
GenericFK Serialization¶
GenericFKOutMixin serializes ContentType FK to "app_label.model_name":
API Documentation Standards¶
Endpoint Decorators¶
Every endpoint must include operation_id and summary:
@http_post(
"/{uuid:loan_id}/approve",
response={200: LoanOut, 404: ErrorOut, 409: ErrorOut},
permissions=[IsLoanOfficerOrAbove],
operation_id="approve_loan",
summary="Approve a loan",
)
async def approve(self, loan_id: UUID, payload: LoanApproveIn) -> Any:
"""Approve a pending loan application.
Runs compliance checks before approval."""
operation_id:{verb}_{resource}format, unique across all endpointssummary: Short imperative phrase- Docstring: Multi-line for complex business actions
Ruff Configuration¶
[tool.ruff]
target-version = "py313"
line-length = 120
extend-exclude = ["*/migrations/*"]
[tool.ruff.lint]
select = ["E", "W", "F", "I", "N", "UP", "B", "A", "DJ", "RUF"]
mypy Configuration¶
[tool.mypy]
python_version = "3.13"
plugins = ["mypy_django_plugin.main"]
strict = true
warn_unreachable = true
Tests relax strict typing (disallow_untyped_defs = false). Migrations are ignored entirely.
See Also¶
- Layered Architecture --- Layer dependency rules
- Testing Guide --- Test patterns
- Adding an App --- Full app creation walkthrough