Django Development Patterns
Opinionated Django development toolkit enforcing consistent, production-ready patterns.
Core Principles
-
One file = one model/form - Each model and form lives in its own file
-
Consistent prefixes - Abstract (Base* ), virtual (Virtual* ), proxy (Proxy* )
-
UUID primary keys - All models use UUID instead of auto-increment
-
Timestamps everywhere - All models inherit created_at/updated_at
-
Soft delete by default - Use deleted_at instead of hard deletes
-
Dynaconf for config - Never use plain settings.py
-
uv + pyproject.toml - Use uv for package management with split deps
-
Class member ordering - Strict ordering for readability
-
Docker in /docker - All Docker artifacts in /docker folder
Project Setup (uv + pyproject.toml)
Always use uv for package management with split dependencies:
Initialize project
uv init myproject cd myproject
Add dependencies by group
uv add django dynaconf django-unfold django-ninja uv add --group dev ruff mypy django-stubs uv add --group test pytest pytest-django factory-boy pytest-cov
pyproject.toml :
[project] name = "myproject" version = "0.1.0" requires-python = ">=3.12" dependencies = [ "django>=5.0", "dynaconf[toml]>=3.2", "django-unfold>=0.30", "django-ninja>=1.0", "psycopg[binary]>=3.1", "whitenoise>=6.6", ]
[dependency-groups] dev = [ "ruff>=0.3", "mypy>=1.8", "django-stubs>=4.2", "ipython>=8.0", ] test = [ "pytest>=8.0", "pytest-django>=4.8", "factory-boy>=3.3", "pytest-cov>=4.1", "freezegun>=1.4", ]
[tool.ruff] line-length = 100 target-version = "py312"
[tool.mypy] plugins = ["mypy_django_plugin.main"] strict = true
Project Structure
Standard Django project layout:
project/ ├── pyproject.toml # Dependencies (uv) ├── uv.lock # Lock file ├── docker/ │ ├── Dockerfile # Main Dockerfile │ ├── Dockerfile.dev # Development Dockerfile │ ├── docker-compose.yml # Main compose │ ├── docker-compose.dev.yml │ ├── nginx/ │ │ └── nginx.conf │ └── scripts/ │ ├── entrypoint.sh │ └── wait-for-it.sh ├── config/ │ ├── init.py │ ├── settings.py # Dynaconf integration │ ├── .secrets.toml # Gitignored secrets │ └── settings.toml # Environment config ├── apps/ │ └── myapp/ │ ├── models/ # Package, not single file │ │ ├── init.py │ │ ├── base.py # Base classes │ │ └── user.py # One model per file │ ├── forms/ │ │ ├── init.py │ │ └── user.py │ ├── managers/ │ │ ├── init.py │ │ └── user.py │ ├── api/ # Django Ninja (see django-dev-ninja) │ └── admin/ # Unfold admin (see django-dev-unfold) ├── tests/ # See django-dev-test └── manage.py
Docker Configuration
All Docker artifacts in /docker folder:
docker/Dockerfile
FROM python:3.12-slim
WORKDIR /app
Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
Install dependencies
COPY pyproject.toml uv.lock ./ RUN uv sync --frozen --no-dev
Copy application
COPY . .
Collect static files
RUN uv run python manage.py collectstatic --noinput
EXPOSE 8000 CMD ["uv", "run", "gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
docker/docker-compose.yml
services: web: build: context: .. dockerfile: docker/Dockerfile ports: - "8000:8000" environment: - DJANGO_ENV=production env_file: - ../.env depends_on: - db
db: image: postgres:16-alpine volumes: - postgres_data:/var/lib/postgresql/data environment: - POSTGRES_DB=myproject - POSTGRES_USER=postgres - POSTGRES_PASSWORD=${DB_PASSWORD}
volumes: postgres_data:
Model Organization
Base Classes
Create base classes in models/base.py :
import uuid from django.db import models from django.utils import timezone
class BaseTimeStamped(models.Model): """Adds created_at and updated_at timestamps.""" created_at = models.DateTimeField(auto_now_add=True, db_index=True) updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
get_latest_by = "created_at"
class BaseSoftDelete(models.Model): """Adds soft delete capability with deleted_at field.""" deleted_at = models.DateTimeField(null=True, blank=True, db_index=True)
class Meta:
abstract = True
def delete(self, using=None, keep_parents=False):
self.deleted_at = timezone.now()
self.save(update_fields=["deleted_at"])
def hard_delete(self):
super().delete()
@property
def is_deleted(self) -> bool:
return self.deleted_at is not None
class BaseUUID(models.Model): """Uses UUID as primary key instead of auto-increment.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
class Meta:
abstract = True
class BaseModel(BaseUUID, BaseTimeStamped, BaseSoftDelete): """Standard base model with UUID, timestamps, and soft delete."""
class Meta:
abstract = True
Naming Conventions
Prefix Type Example
Base*
Abstract base class BaseTimeStamped , BaseModel
Virtual*
In-memory only (not persisted) VirtualCart , VirtualSession
Proxy*
Proxy model ProxyActiveUser , ProxyAdmin
(none) Regular model User , Product , Order
Class Member Ordering
All classes follow strict member ordering:
-
class Meta
-
ALWAYS FIRST in the class
-
Fields - Class attributes (model fields)
-
Managers - objects = Manager()
-
Properties (@property ) - Alphabetical order
-
Private/dunder methods (_method , str ) - Alphabetical order
-
Public methods - Alphabetical order
class User(BaseModel): """User account model."""
# 1. class Meta - ALWAYS FIRST
class Meta:
db_table = "users"
ordering = ["-created_at"]
# 2. Fields
email = models.EmailField(unique=True)
name = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
# 3. Manager
objects = UserManager()
# 4. Properties (alphabetical)
@property
def display_name(self) -> str:
return self.name or self.email.split("@")[0]
@property
def is_verified(self) -> bool:
return self.email_verified_at is not None
# 5. Private/dunder methods (alphabetical)
def __repr__(self) -> str:
return f"<User {self.email}>"
def __str__(self) -> str:
return self.email
def _calculate_score(self) -> int:
return len(self.orders.all())
def _validate_status(self) -> bool:
return self.is_active
# 6. Public methods (alphabetical)
def activate(self) -> None:
self.is_active = True
self.save(update_fields=["is_active"])
def can_place_order(self) -> bool:
return self.is_active and not self.is_deleted
def deactivate(self) -> None:
self.is_active = False
self.save(update_fields=["is_active"])
Model File Template
Each model in its own file (models/user.py ):
from django.db import models from .base import BaseModel from ..managers.user import UserManager
class User(BaseModel): """User account model."""
# 1. class Meta - ALWAYS FIRST
class Meta:
db_table = "users"
verbose_name = "User"
verbose_name_plural = "Users"
ordering = ["-created_at"]
# 2. Fields
email = models.EmailField(unique=True)
name = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
# 3. Manager
objects = UserManager()
# 4. Properties
@property
def display_name(self) -> str:
return self.name or self.email.split("@")[0]
# 5. Private/dunder methods
def __str__(self) -> str:
return self.email
Model Package Init
Re-export all models in models/init.py :
from .base import BaseModel, BaseTimeStamped, BaseSoftDelete, BaseUUID from .user import User from .product import Product
all = [ "BaseModel", "BaseTimeStamped", "BaseSoftDelete", "BaseUUID", "User", "Product", ]
Custom Managers
Place managers in managers/ package:
managers/user.py
from django.db import models
class UserQuerySet(models.QuerySet): def active(self): return self.filter(is_active=True, deleted_at__isnull=True)
def by_email(self, email: str):
return self.filter(email__iexact=email)
class UserManager(models.Manager): def get_queryset(self) -> UserQuerySet: return UserQuerySet(self.model, using=self._db)
def active(self):
return self.get_queryset().active()
def by_email(self, email: str):
return self.get_queryset().by_email(email)
Dynaconf Configuration
Always use Dynaconf for Django settings. See references/dynaconf.md for complete setup.
Quick setup:
pip install dynaconf dynaconf init -f toml
Update config/settings.py :
from dynaconf import Dynaconf
settings = Dynaconf( envvar_prefix="DJANGO", settings_files=["settings.toml", ".secrets.toml"], environments=True, env_switcher="DJANGO_ENV", )
Form Organization
Forms follow the same 1-file-per-form pattern. See references/forms.md for details.
forms/user.py
from django import forms from ..models import User
class UserForm(forms.ModelForm): class Meta: model = User fields = ["email", "name"]
Creating a New App
To create a new Django app with proper structure:
- Create app directory with packages:
mkdir -p apps/myapp/{models,forms,managers,api,admin} touch apps/myapp/init.py touch apps/myapp/{models,forms,managers,api,admin}/init.py
-
Create base classes in models/base.py
-
Add app to INSTALLED_APPS using Dynaconf
-
Create initial models following conventions
Additional Resources
Reference Files
For detailed patterns and setup guides:
-
references/models.md
-
Advanced model patterns, relationships, constraints
-
references/forms.md
-
Form organization, validation, widgets
-
references/dynaconf.md
-
Complete Dynaconf setup and environment configuration
Related Skills
-
django-dev-ninja - API development with Django Ninja
-
django-dev-unfold - Admin customization with Unfold
-
django-dev-test - Testing with pytest and factories