django Best Practices
This guide outlines the definitive best practices for developing Django applications, ensuring maintainability, performance, and security. Adhere to these rules for consistent, high-quality code.
1. Code Organization & Project Structure
Adopt the src-layout for clear separation of concerns. Keep apps focused on a single domain.
Project Layout (src-layout):
.
├── manage.py
├── src/
│ ├── config/ # Project-level settings, URLs, WSGI/ASGI
│ │ ├── __init__.py
│ │ ├── settings/
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ ├── development.py
│ │ │ └── production.py
│ │ └── urls.py
│ ├── apps/ # Domain-driven Django apps
│ │ ├── users/
│ │ │ ├── models.py
│ │ │ ├── views.py
│ │ │ └── tests/
│ │ ├── products/
│ │ └── ...
│ └── common/ # Reusable utilities, abstract base models, etc.
└── requirements.txt
Split Settings: Use django-environ for environment-specific settings. Never commit secrets.
❌ BAD: Hardcoding secrets, single settings.py
# config/settings.py
DEBUG = True
SECRET_KEY = 'super-secret-dev-key'
DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3'}}
✅ GOOD: Environment variables, split files
# src/config/settings/base.py
import environ
env = environ.Env()
environ.Env.read_env() # reads .env file
SECRET_KEY = env('SECRET_KEY')
DEBUG = env.bool('DEBUG', default=False)
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[])
# src/config/settings/development.py
from .base import *
DEBUG = True
DATABASES = {'default': env.db('DATABASE_URL', default='sqlite:///db.sqlite3')}
# .env file (not committed to VCS)
SECRET_KEY=your_actual_secret_key
DEBUG=True
DATABASE_URL=postgres://user:pass@host:port/dbname
Model Naming: Models are singular nouns. related_name for reverse relationships is plural.
❌ BAD:
class Users(models.Model): pass
owner = models.ForeignKey(Owner, related_name='item')
✅ GOOD:
class User(models.Model): pass
owner = models.ForeignKey(Owner, related_name='items',
2. Common Patterns & Anti-patterns
Fat Models, Skinny Views: Business logic belongs in models or dedicated service layers, not views. Views orchestrate, models/services execute.
❌ BAD: Logic in view
# apps/orders/views.py
def create_order_view(request):
# ... validation ...
product = Product.objects.get(id=product_id)
if product.stock < quantity:
raise ValidationError("Not enough stock")
order = Order.objects.create(user=request.user, product=product, quantity=quantity)
product.stock -= quantity
product.save()
# ...
✅ GOOD: Logic in model/service
# apps/orders/models.py
class Order(models.Model):
# ... fields ...
@classmethod
def create_with_stock_check(cls, user, product, quantity):
if product.stock < quantity:
raise ValidationError("Not enough stock")
order = cls.objects.create(user=user, product=product, quantity=quantity)
product.stock -= quantity
product.save()
return order
# apps/orders/views.py
def create_order_view(request):
# ... validation ...
order = Order.create_with_stock_check(request.user, product, quantity)
# ...
3. Performance Considerations
Optimize ORM Queries: Avoid N+1 queries.
❌ BAD: N+1 query
# Iterates over users, then queries profile for each
users = User.objects.all()
for user in users:
print(user.profile.bio)
✅ GOOD: select_related (one-to-one, foreign key)
users = User.objects.select_related('profile').all()
for user in users:
print(user.profile.bio)
✅ GOOD: prefetch_related (many-to-many, reverse foreign key)
# For a list of books, prefetch all authors for each book
books = Book.objects.prefetch_related('authors').all()
for book in books:
print([author.name for author in book.authors.all()])
Database Indexes: Add db_index=True to frequently filtered/ordered fields.
class MyModel(models.Model):
name = models.CharField(max_length=100, db_index=True) # Indexed
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
Async ORM: Use sync_to_async for blocking ORM calls in async views, or the native async ORM (Django 4.1+).
# In an async view
from asgiref.sync import sync_to_async
async def my_async_view(request):
# Blocking ORM call, wrap it
user = await sync_to_async(User.objects.get)(id=request.user.id)
# Or, if using native async ORM (Django 4.1+):
# user = await User.objects.aget(id=request.user.id)
return JsonResponse({'username': user.username})
4. Security Best Practices
Secrets Management: Never commit secrets to VCS. Use environment variables (see Split Settings).
Permissions (DRF): Implement role-based permissions using DRF's permission_classes.
❌ BAD: Manual checks in view
class MyView(APIView):
def get(self, request):
if not request.user.is_staff:
return Response(status=403)
# ...
✅ GOOD: DRF Permission Classes
from rest_framework.permissions import IsAdminUser
class MyView(APIView):
permission_classes = [IsAdminUser] # Only staff can access
def get(self, request):
# ...
Static & Media Files: Serve static files with ManifestStaticFilesStorage and media files from cloud storage (e.g., S3, GCS).
# config/settings/production.py
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' # e.g., django-storages
5. API Design (DRF)
ViewSets & Routers: Use ModelViewSet for CRUD operations to reduce boilerplate.
❌ BAD: Separate views for list and detail
class ProductListView(APIView): pass
class ProductDetailView(APIView): pass
# urls.py: path('products/', ProductListView.as_view()), path('products/<int:pk>/', ProductDetailView.as_view())
✅ GOOD: ModelViewSet with Router
# apps/products/views.py
from rest_framework import viewsets
from .models import Product
from .serializers import ProductSerializer
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
# src/config/urls.py
from rest_framework.routers import DefaultRouter
from apps.products.views import ProductViewSet
router = DefaultRouter()
router.register(r'products', ProductViewSet)
urlpatterns = [
# ...
path('api/', include(router.urls)),
]
6. Type Hints
Always use type hints for improved readability, maintainability, and static analysis with mypy.
❌ BAD: Untyped function
def calculate_total(price, quantity):
return price * quantity
✅ GOOD: Typed function
def calculate_total(price: float, quantity: int) -> float:
return price * quantity
# For models:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from django.db.models import QuerySet
class Product(models.Model):
name: str = models.CharField(max_length=255)
price: float = models.DecimalField(max_digits=10, decimal_places=2)
def get_related_products(self) -> "QuerySet[Product]":
# ...
return Product.objects.filter(...)
1---2name: django3description: "[Applies to: **/*.py] Definitive guidelines for writing maintainable, performant, and secure Django applications, emphasizing modern best practices, clear code organization, and efficient patterns."4source: "cursor_mdc"5---67# django Best Practices89This guide outlines the definitive best practices for developing Django applications, ensuring maintainability, performance, and security. Adhere to these rules for consistent, high-quality code.1011## 1. Code Organization & Project Structure1213Adopt the `src-layout` for clear separation of concerns. Keep apps focused on a single domain.1415* **Project Layout (`src-layout`)**:16 ```17 .18 ├── manage.py19 ├── src/20 │ ├── config/ # Project-level settings, URLs, WSGI/ASGI21 │ │ ├── __init__.py22 │ │ ├── settings/23 │ │ │ ├── __init__.py24 │ │ │ ├── base.py25 │ │ │ ├── development.py26 │ │ │ └── production.py27 │ │ └── urls.py28 │ ├── apps/ # Domain-driven Django apps29 │ │ ├── users/30 │ │ │ ├── models.py31 │ │ │ ├── views.py32 │ │ │ └── tests/33 │ │ ├── products/34 │ │ └── ...35 │ └── common/ # Reusable utilities, abstract base models, etc.36 └── requirements.txt37 ```3839* **Split Settings**: Use `django-environ` for environment-specific settings. Never commit secrets.4041 ❌ BAD: Hardcoding secrets, single `settings.py`42 ```python43 # config/settings.py44 DEBUG = True45 SECRET_KEY = 'super-secret-dev-key'46 DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3'}}47 ```4849 ✅ GOOD: Environment variables, split files50 ```python51 # src/config/settings/base.py52 import environ53 env = environ.Env()54 environ.Env.read_env() # reads .env file5556 SECRET_KEY = env('SECRET_KEY')57 DEBUG = env.bool('DEBUG', default=False)58 ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[])5960 # src/config/settings/development.py61 from .base import *62 DEBUG = True63 DATABASES = {'default': env.db('DATABASE_URL', default='sqlite:///db.sqlite3')}6465 # .env file (not committed to VCS)66 SECRET_KEY=your_actual_secret_key67 DEBUG=True68 DATABASE_URL=postgres://user:pass@host:port/dbname69 ```7071* **Model Naming**: Models are singular nouns. `related_name` for reverse relationships is plural.7273 ❌ BAD:74 ```python75 class Users(models.Model): pass76 owner = models.ForeignKey(Owner, related_name='item')77 ```7879 ✅ GOOD:80 ```python81 class User(models.Model): pass82 owner = models.ForeignKey(Owner, related_name='items', on_delete=models.CASCADE)83 ```8485## 2. Common Patterns & Anti-patterns8687* **Fat Models, Skinny Views**: Business logic belongs in models or dedicated service layers, not views. Views orchestrate, models/services execute.8889 ❌ BAD: Logic in view90 ```python91 # apps/orders/views.py92 def create_order_view(request):93 # ... validation ...94 product = Product.objects.get(id=product_id)95 if product.stock < quantity:96 raise ValidationError("Not enough stock")97 order = Order.objects.create(user=request.user, product=product, quantity=quantity)98 product.stock -= quantity99 product.save()100 # ...101 ```102103 ✅ GOOD: Logic in model/service104 ```python105 # apps/orders/models.py106 class Order(models.Model):107 # ... fields ...108 @classmethod109 def create_with_stock_check(cls, user, product, quantity):110 if product.stock < quantity:111 raise ValidationError("Not enough stock")112 order = cls.objects.create(user=user, product=product, quantity=quantity)113 product.stock -= quantity114 product.save()115 return order116117 # apps/orders/views.py118 def create_order_view(request):119 # ... validation ...120 order = Order.create_with_stock_check(request.user, product, quantity)121 # ...122 ```123124## 3. Performance Considerations125126* **Optimize ORM Queries**: Avoid N+1 queries.127128 ❌ BAD: N+1 query129 ```python130 # Iterates over users, then queries profile for each131 users = User.objects.all()132 for user in users:133 print(user.profile.bio)134 ```135136 ✅ GOOD: `select_related` (one-to-one, foreign key)137 ```python138 users = User.objects.select_related('profile').all()139 for user in users:140 print(user.profile.bio)141 ```142143 ✅ GOOD: `prefetch_related` (many-to-many, reverse foreign key)144 ```python145 # For a list of books, prefetch all authors for each book146 books = Book.objects.prefetch_related('authors').all()147 for book in books:148 print([author.name for author in book.authors.all()])149 ```150151* **Database Indexes**: Add `db_index=True` to frequently filtered/ordered fields.152 ```python153 class MyModel(models.Model):154 name = models.CharField(max_length=100, db_index=True) # Indexed155 created_at = models.DateTimeField(auto_now_add=True, db_index=True)156 ```157158* **Async ORM**: Use `sync_to_async` for blocking ORM calls in async views, or the native async ORM (Django 4.1+).159160 ```python161 # In an async view162 from asgiref.sync import sync_to_async163164 async def my_async_view(request):165 # Blocking ORM call, wrap it166 user = await sync_to_async(User.objects.get)(id=request.user.id)167 # Or, if using native async ORM (Django 4.1+):168 # user = await User.objects.aget(id=request.user.id)169 return JsonResponse({'username': user.username})170 ```171172## 4. Security Best Practices173174* **Secrets Management**: Never commit secrets to VCS. Use environment variables (see Split Settings).175176* **Permissions (DRF)**: Implement role-based permissions using DRF's `permission_classes`.177178 ❌ BAD: Manual checks in view179 ```python180 class MyView(APIView):181 def get(self, request):182 if not request.user.is_staff:183 return Response(status=403)184 # ...185 ```186187 ✅ GOOD: DRF Permission Classes188 ```python189 from rest_framework.permissions import IsAdminUser190191 class MyView(APIView):192 permission_classes = [IsAdminUser] # Only staff can access193 def get(self, request):194 # ...195 ```196197* **Static & Media Files**: Serve static files with `ManifestStaticFilesStorage` and media files from cloud storage (e.g., S3, GCS).198199 ```python200 # config/settings/production.py201 STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'202 DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' # e.g., django-storages203 ```204205## 5. API Design (DRF)206207* **ViewSets & Routers**: Use `ModelViewSet` for CRUD operations to reduce boilerplate.208209 ❌ BAD: Separate views for list and detail210 ```python211 class ProductListView(APIView): pass212 class ProductDetailView(APIView): pass213 # urls.py: path('products/', ProductListView.as_view()), path('products/<int:pk>/', ProductDetailView.as_view())214 ```215216 ✅ GOOD: `ModelViewSet` with Router217 ```python218 # apps/products/views.py219 from rest_framework import viewsets220 from .models import Product221 from .serializers import ProductSerializer222223 class ProductViewSet(viewsets.ModelViewSet):224 queryset = Product.objects.all()225 serializer_class = ProductSerializer226227 # src/config/urls.py228 from rest_framework.routers import DefaultRouter229 from apps.products.views import ProductViewSet230231 router = DefaultRouter()232 router.register(r'products', ProductViewSet)233 urlpatterns = [234 # ...235 path('api/', include(router.urls)),236 ]237 ```238239## 6. Type Hints240241Always use type hints for improved readability, maintainability, and static analysis with `mypy`.242243❌ BAD: Untyped function244```python245def calculate_total(price, quantity):246 return price * quantity247```248249✅ GOOD: Typed function250```python251def calculate_total(price: float, quantity: int) -> float:252 return price * quantity253254# For models:255from typing import TYPE_CHECKING256if TYPE_CHECKING:257 from django.db.models import QuerySet258259class Product(models.Model):260 name: str = models.CharField(max_length=255)261 price: float = models.DecimalField(max_digits=10, decimal_places=2)262263 def get_related_products(self) -> "QuerySet[Product]":264 # ...265 return Product.objects.filter(...)266```