Django Conventions and Best Practices
Purpose
This skill provides comprehensive Django best practices and conventions to ensure high-quality, secure, and performant Django applications. It serves as a reference guide during code reviews to verify adherence to Django standards and community best practices.
When to use this skill:
- Conducting code reviews of Django projects
- Designing Django applications and models
- Writing Django views, serializers, and forms
- Evaluating Django security and performance
- Refactoring Django codebases
- Teaching Django best practices to team members
This skill is designed to be referenced by the uncle-duke-python agent during Django code reviews.
Context
Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. This skill documents industry-standard Django practices that emphasize:
- Convention over Configuration: Follow Django's conventions for predictability
- Don't Repeat Yourself (DRY): Minimize code duplication
- Explicit is better than implicit: Clear, readable code
- Security by default: Leverage Django's built-in security features
- Database efficiency: Optimize queries and avoid common performance pitfalls
- Maintainability: Write code that's easy to understand and modify
Prerequisites
Required Knowledge:
- Python fundamentals and best practices
- Understanding of web development concepts (HTTP, REST, MVC/MTV)
- Basic understanding of Django's MTV (Model-Template-View) architecture
- SQL and database concepts
Required Tools:
- Django 3.2+ (LTS recommended)
- Python 3.8+
- Database (PostgreSQL recommended for production)
Expected Project Structure:
myproject/
├── manage.py
├── myproject/ # Project configuration
│ ├── __init__.py
│ ├── settings/ # Split settings by environment
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── development.py
│ │ ├── production.py
│ │ └── test.py
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── apps/ # Django apps
│ ├── users/
│ │ ├── __init__.py
│ │ ├── models.py
│ │ ├── views.py
│ │ ├── serializers.py
│ │ ├── urls.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── managers.py
│ │ ├── tests/
│ │ │ ├── test_models.py
│ │ │ ├── test_views.py
│ │ │ └── test_serializers.py
│ │ └── migrations/
│ └── core/
├── static/
├── media/
├── templates/
├── requirements/
│ ├── base.txt
│ ├── development.txt
│ ├── production.txt
│ └── test.txt
└── README.md
Instructions
Task 1: Django Project Structure Best Practices
1.1 Project Layout
Rule: Organize Django projects with clear separation between project configuration and apps.
✅ Good Project Structure:
myproject/
├── manage.py
├── myproject/ # Project settings and configuration
│ ├── settings/
│ │ ├── base.py # Shared settings
│ │ ├── development.py # Dev-specific settings
│ │ ├── production.py # Production settings
│ │ └── test.py # Test settings
│ ├── urls.py # Root URL configuration
│ ├── wsgi.py
│ └── asgi.py
├── apps/ # All Django apps
│ ├── users/
│ ├── blog/
│ └── core/ # Shared utilities
├── static/ # Static files
├── media/ # User-uploaded files
├── templates/ # Shared templates
├── requirements/ # Split requirements
└── docs/ # Documentation
❌ Bad:
myproject/
├── manage.py
├── settings.py # All settings in one file
├── users.py # Apps not properly organized
├── blog.py
└── utils.py # Mixed concerns
Why: Clear structure improves maintainability, makes settings management easier, and follows Django community standards.
1.2 App Organization
Rule: Each app should be focused on a single domain concept.
✅ Good App Structure:
users/
├── __init__.py
├── models.py # User-related models
├── views.py # User views
├── serializers.py # DRF serializers
├── urls.py # App-specific URLs
├── admin.py # Admin configuration
├── apps.py # App configuration
├── managers.py # Custom model managers
├── forms.py # Forms
├── signals.py # Signal handlers
├── permissions.py # Custom permissions
├── utils.py # App-specific utilities
├── tests/
│ ├── __init__.py
│ ├── test_models.py
│ ├── test_views.py
│ └── factories.py # Test factories
└── migrations/
App Naming Conventions:
- Use plural nouns for apps containing models (users, posts, comments)
- Use singular nouns for utility apps (core, common, utils)
- Keep app names short and descriptive
- Use underscores for multi-word names (user_profiles)
1.3 Settings Organization
Rule: Split settings by environment for security and flexibility.
✅ Good Settings Structure:
settings/base.py:
"""Base settings shared across all environments."""
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# SECURITY WARNING: keep the secret key used in production secret!
# This should be overridden in environment-specific settings
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'dev-only-secret-key')
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third-party apps
'rest_framework',
'django_filters',
# Local apps
'apps.users',
'apps.blog',
'apps.core',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'myproject.urls'
# Internationalization
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True # Always use timezone-aware datetimes
# Static files
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Default primary key field type
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
settings/development.py:
"""Development-specific settings."""
from .base import *
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'myproject_dev',
'USER': 'myproject_user',
'PASSWORD': 'dev_password',
'HOST': 'localhost',
'PORT': '5432',
}
}
# Development-specific apps
INSTALLED_APPS += [
'debug_toolbar',
'django_extensions',
]
MIDDLEWARE += [
'debug_toolbar.middleware.DebugToolbarMiddleware',
]
# Django Debug Toolbar
INTERNAL_IPS = ['127.0.0.1']
# Email backend for development
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
settings/production.py:
"""Production settings."""
import os
from .base import *
DEBUG = False
# SECURITY WARNING: Update this to your domain
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
# Use environment variables for sensitive data
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ['DB_NAME'],
'USER': os.environ['DB_USER'],
'PASSWORD': os.environ['DB_PASSWORD'],
'HOST': os.environ['DB_HOST'],
'PORT': os.environ.get('DB_PORT', '5432'),
'CONN_MAX_AGE': 600, # Connection pooling
}
}
# Security settings
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Logging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'ERROR',
'class': 'logging.FileHandler',
'filename': '/var/log/myproject/django.log',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'ERROR',
'propagate': True,
},
},
}
1.4 URL Configuration Patterns
Rule: Use RESTful URL patterns and include() for app-specific URLs.
✅ Good:
myproject/urls.py:
"""Root URL configuration."""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/users/', include('apps.users.urls')),
path('api/v1/blog/', include('apps.blog.urls')),
path('api/v1/', include('apps.core.urls')),
]
# Serve media files in development
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
apps/users/urls.py:
"""User app URL configuration."""
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
app_name = 'users' # URL namespace
router = DefaultRouter()
router.register(r'', views.UserViewSet, basename='user')
urlpatterns = [
path('', include(router.urls)),
path('me/', views.CurrentUserView.as_view(), name='current-user'),
path('login/', views.LoginView.as_view(), name='login'),
path('logout/', views.LogoutView.as_view(), name='logout'),
]
URL Naming Best Practices:
- Use lowercase with hyphens:
/api/user-profiles/ - Version your APIs:
/api/v1/,/api/v2/ - Use plural nouns for resources:
/users/,/posts/ - Use nested routes sparingly:
/users/123/posts/(consider/posts/?user=123instead) - Always name your URLs for reverse lookup
Task 2: Model Best Practices
2.1 Model Design Patterns
Rule: Models should be focused, well-documented, and follow Django conventions.
✅ Good Model Design:
"""User models."""
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator
class User(AbstractUser):
"""Custom user model extending Django's AbstractUser.
Adds additional fields for user profiles and implements
business logic related to user accounts.
"""
class UserRole(models.TextChoices):
"""User role choices."""
ADMIN = 'ADMIN', _('Administrator')
MODERATOR = 'MOD', _('Moderator')
USER = 'USER', _('Regular User')
# Additional fields
email = models.EmailField(_('email address'), unique=True)
role = models.CharField(
_('role'),
max_length=10,
choices=UserRole.choices,
default=UserRole.USER,
)
bio = models.TextField(_('biography'), blank=True, max_length=500)
birth_date = models.DateField(_('birth date'), null=True, blank=True)
avatar = models.ImageField(
_('avatar'),
upload_to='avatars/%Y/%m/%d/',
null=True,
blank=True,
)
email_verified = models.BooleanField(_('email verified'), default=False)
created_at = models.DateTimeField(_('created at'), auto_now_add=True)
updated_at = models.DateTimeField(_('updated at'), auto_now=True)
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
ordering = ['-created_at']
indexes = [
models.Index(fields=['email']),
models.Index(fields=['-created_at']),
]
def __str__(self):
"""String representation of user."""
return f"{self.username} ({self.get_role_display()})"
def get_full_name(self):
"""Return user's full name or username if not set."""
full_name = super().get_full_name()
return full_name if full_name else self.username
@property
def is_admin(self):
"""Check if user has admin role."""
return self.role == self.UserRole.ADMIN
def verify_email(self):
"""Mark user's email as verified."""
self.email_verified = True
self.save(update_fields=['email_verified', 'updated_at'])
class Post(models.Model):
"""Blog post model."""
class PostStatus(models.TextChoices):
"""Post status choices."""
DRAFT = 'DRAFT', _('Draft')
PUBLISHED = 'PUBLISHED', _('Published')
ARCHIVED = 'ARCHIVED', _('Archived')
title = models.CharField(_('title'), max_length=200)
slug = models.SlugField(_('slug'), max_length=200, unique=True)
author = models.ForeignKey(
User,
related_name='posts',
related_query_name='post',
verbose_name=_('author'),
)
content = models.TextField(_('content'))
status = models.CharField(
_('status'),
max_length=20,
choices=PostStatus.choices,
default=PostStatus.DRAFT,
db_index=True,
)
featured = models.BooleanField(_('featured'), default=False)
view_count = models.PositiveIntegerField(_('view count'), default=0)
published_at = models.DateTimeField(_('published at'), null=True, blank=True)
created_at = models.DateTimeField(_('created at'), auto_now_add=True)
updated_at = models.DateTimeField(_('updated at'), auto_now=True)
# Use custom manager
objects = PostManager()
class Meta:
verbose_name = _('post')
verbose_name_plural = _('posts')
ordering = ['-published_at', '-created_at']
indexes = [
models.Index(fields=['status', '-published_at']),
models.Index(fields=['author', '-created_at']),
]
constraints = [
models.CheckConstraint(
check=models.Q(view_count__gte=0),
name='post_view_count_non_negative',
),
]
def __str__(self):
"""String representation of post."""
return self.title
def save(self, *args, **kwargs):
"""Override save to set published_at when status changes to published."""
if self.status == self.PostStatus.PUBLISHED and not self.published_at:
from django.utils import timezone
self.published_at = timezone.now()
super().save(*args, **kwargs)
def increment_view_count(self):
"""Increment post view count efficiently."""
self.__class__.objects.filter(pk=self.pk).update(
view_count=models.F('view_count') + 1
)
# Refresh from database
self.refresh_from_db(fields=['view_count'])
Key Model Design Principles:
- Use verbose field names with gettext_lazy for i18n
- Add
help_textfor complex fields - Use TextChoices/IntegerChoices for choice fields
- Include timestamps (created_at, updated_at) on most models
- Use appropriate
on_deletefor ForeignKey - Set
related_nameandrelated_query_nameon relationships - Add database indexes for frequently queried fields
- Use constraints for data integrity
- Override
__str__()for meaningful representations - Document the model and complex methods
2.2 Field Choices and Naming
Rule: Use TextChoices/IntegerChoices for field choices, follow naming conventions.
✅ Good:
class Order(models.Model):
"""Customer order model."""
class OrderStatus(models.TextChoices):
"""Order status choices using TextChoices."""
PENDING = 'PENDING', _('Pending Payment')
PAID = 'PAID', _('Paid')
PROCESSING = 'PROCESSING', _('Processing')
SHIPPED = 'SHIPPED', _('Shipped')
DELIVERED = 'DELIVERED', _('Delivered')
CANCELLED = 'CANCELLED', _('Cancelled')
REFUNDED = 'REFUNDED', _('Refunded')
class PaymentMethod(models.TextChoices):
"""Payment method choices."""
CREDIT_CARD = 'CC', _('Credit Card')
DEBIT_CARD = 'DC', _('Debit Card')
PAYPAL = 'PP', _('PayPal')
BANK_TRANSFER = 'BT', _('Bank Transfer')
# Field naming follows snake_case
order_number = models.CharField(max_length=50, unique=True)
customer = models.ForeignKey(User,
status = models.CharField(
max_length=20,
choices=OrderStatus.choices,
default=OrderStatus.PENDING,
)
payment_method = models.CharField(
max_length=2,
choices=PaymentMethod.choices,
null=True,
blank=True,
)
total_amount = models.DecimalField(
max_digits=10,
decimal_places=2,
validators=[MinValueValidator(0)],
)
shipping_address = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Order {self.order_number} - {self.get_status_display()}"
❌ Bad:
class Order(models.Model):
"""Bad example - avoid this."""
# Bad: Tuple choices instead of TextChoices
STATUS_CHOICES = (
(1, 'Pending'),
(2, 'Paid'),
(3, 'Shipped'),
)
# Bad: Using integers without clear meaning
status = models.IntegerField(choices=STATUS_CHOICES)
# Bad: camelCase instead of snake_case
orderNumber = models.CharField(max_length=50)
# Bad: Vague field names
amt = models.DecimalField(max_digits=10, decimal_places=2)
addr = models.TextField()
Field Naming Conventions:
- Use snake_case for field names
- Be explicit and descriptive (avoid abbreviations)
- Use
_idsuffix sparingly (Django adds it automatically to ForeignKey) - Use boolean field names that read like questions:
is_active,has_paid,email_verified - Use date/time field names with
_ator_datesuffix:created_at,birth_date
2.3 Meta Class Options
Rule: Use Meta class to configure model behavior and database options.
✅ Good Meta Class:
class Article(models.Model):
"""Article model with comprehensive Meta configuration."""
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
author = models.ForeignKey(User,
category = models.ForeignKey('Category', null=True)
published_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
# Verbose names for admin
verbose_name = _('article')
verbose_name_plural = _('articles')
# Default ordering
ordering = ['-published_at', '-created_at']
# Get latest by
get_latest_by = 'published_at'
# Database table name (optional, Django auto-generates)
db_table = 'blog_articles'
# Indexes for query optimization
indexes = [
models.Index(fields=['slug']),
models.Index(fields=['author', '-published_at']),
models.Index(fields=['category', '-published_at']),
models.Index(fields=['-published_at'], name='recent_articles_idx'),
]
# Unique together constraints
constraints = [
models.UniqueConstraint(
fields=['author', 'slug'],
name='unique_author_slug',
),
models.CheckConstraint(
check=models.Q(published_at__isnull=True) | models.Q(published_at__gte=models.F('created_at')),
name='published_after_created',
),
]
# Permissions
permissions = [
('can_publish', 'Can publish articles'),
('can_feature', 'Can feature articles'),
]
Common Meta Options:
verbose_name/verbose_name_plural: Admin display namesordering: Default query orderingindexes: Database indexes for performanceconstraints: UniqueConstraint, CheckConstraint for data integritypermissions: Custom permissionsdb_table: Custom table name (use sparingly)get_latest_by: Field to use for latest()abstract: For abstract base modelsmanaged: Whether Django manages database lifecycle
2.4 Managers and QuerySets
Rule: Use custom managers for reusable query logic and QuerySets for chainable queries.
✅ Good Custom Manager and QuerySet:
apps/blog/managers.py:
"""Custom managers and querysets for blog app."""
from django.db import models
from django.utils import timezone
class PostQuerySet(models.QuerySet):
"""Custom QuerySet for Post model with reusable query methods."""
def published(self):
"""Return only published posts."""
return self.filter(
status=self.model.PostStatus.PUBLISHED,
published_at__lte=timezone.now(),
)
def drafts(self):
"""Return draft posts."""
return self.filter(status=self.model.PostStatus.DRAFT)
def by_author(self, author):
"""Return posts by specific author."""
return self.filter(author=author)
def featured(self):
"""Return featured posts."""
return self.filter(featured=True)
def recent(self, days=30):
"""Return posts from last N days."""
cutoff_date = timezone.now() - timezone.timedelta(days=days)
return self.filter(published_at__gte=cutoff_date)
def with_author_info(self):
"""Optimize query by selecting related author."""
return self.select_related('author')
def with_comments_count(self):
"""Annotate with comment count."""
return self.annotate(
comments_count=models.Count('comments', distinct=True)
)
def popular(self, min_views=100):
"""Return popular posts above view threshold."""
return self.filter(view_count__gte=min_views).order_by('-view_count')
class PostManager(models.Manager):
"""Custom manager for Post model."""
def get_queryset(self):
"""Return custom QuerySet."""
return PostQuerySet(self.model, using=self._db)
# Proxy QuerySet methods for convenience
def published(self):
"""Return published posts."""
return self.get_queryset().published()
def drafts(self):
"""Return draft posts."""
return self.get_queryset().drafts()
def by_author(self, author):
"""Return posts by author."""
return self.get_queryset().by_author(author)
def featured(self):
"""Return featured posts."""
return self.get_queryset().featured()
def recent(self, days=30):
"""Return recent posts."""
return self.get_queryset().recent(days)
class PublishedPostManager(models.Manager):
"""Manager that returns only published posts by default."""
def get_queryset(self):
"""Return only published posts."""
return super().get_queryset().filter(
status='PUBLISHED',
published_at__lte=timezone.now(),
)
Usage in models.py:
from .managers import PostManager, PublishedPostManager
class Post(models.Model):
# ... fields ...
# Default manager
objects = PostManager()
# Additional manager for published posts only
published = PublishedPostManager()
class Meta:
base_manager_name = 'objects' # Use for related queries
Usage in views:
# Chainable QuerySet methods
recent_featured_posts = Post.objects.published().featured().recent(days=7)
# Multiple optimizations
popular_posts = (
Post.objects
.published()
.with_author_info()
.with_comments_count()
.popular(min_views=500)
)
# Using alternative manager
all_published = Post.published.all()
Manager Best Practices:
- Put query logic in QuerySets for chainability
- Create manager methods that return QuerySets
- Use descriptive method names
- Document what each method does
- Don't put business logic in managers (use models or services)
- Use
select_related()andprefetch_related()in manager methods
2.5 Model Methods vs Signals
Rule: Use model methods for object-specific logic, signals for cross-cutting concerns.
✅ Good - Use Model Methods:
class Order(models.Model):
"""Order model with business logic in methods."""
total_amount = models.DecimalField(max_digits=10, decimal_places=2)
discount_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
tax_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
def calculate_total(self):
"""Calculate order total including tax and discount."""
subtotal = self.total_amount - self.discount_amount
return subtotal + self.tax_amount
def apply_discount(self, discount_code):
"""Apply discount code to order."""
from .services import DiscountService
discount = DiscountService.validate_and_get_discount(discount_code, self)
self.discount_amount = discount.amount
self.save(update_fields=['discount_amount'])
return discount
def mark_as_paid(self):
"""Mark order as paid and trigger fulfillment."""
self.status = self.OrderStatus.PAID
self.paid_at = timezone.now()
self.save(update_fields=['status', 'paid_at'])
# Trigger fulfillment signal
from .signals import order_paid
order_paid.send(sender=self.__class__, order=self)
✅ Good - Use Signals for Cross-Cutting Concerns:
apps/orders/signals.py:
"""Order-related signals."""
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver, Signal
from .models import Order
# Custom signal
order_paid = Signal() # Provides 'order' argument
@receiver(post_save, sender=Order)
def send_order_confirmation_email(sender, instance, created, **kwargs):
"""Send confirmation email when order is created."""
if created:
from .tasks import send_order_confirmation_email_task
send_order_confirmation_email_task.delay(instance.id)
@receiver(order_paid)
def start_order_fulfillment(sender, order, **kwargs):
"""Start fulfillment process when order is paid."""
from .tasks import start_fulfillment_task
start_fulfillment_task.delay(order.id)
@receiver(pre_delete, sender=Order)
def log_order_deletion(sender, instance, **kwargs):
"""Log when order is deleted for audit trail."""
import logging
logger = logging.getLogger(__name__)
logger.warning(
f"Order {instance.order_number} deleted by system",
extra={'order_id': instance.id}
)
Connect signals in apps.py:
from django.apps import AppConfig
class OrdersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.orders'
def ready(self):
"""Import signals when app is ready."""
import apps.orders.signals # noqa
When to Use Each:
Model Methods:
- Object-specific business logic
- Calculations based on model data
- State transitions
- Data validation
- Simple related object queries
Signals:
- Send notifications (email, SMS, webhooks)
- Update caches
- Create audit logs
- Trigger background tasks
- Cross-app communication
- Update denormalized data
❌ Bad - Business Logic in Signals:
@receiver(post_save, sender=Order)
def update_order_total(sender, instance, **kwargs):
"""DON'T DO THIS - business logic should be in model method."""
instance.total = instance.calculate_subtotal() + instance.tax
instance.save() # Causes infinite loop!
2.6 Related Names and related_query_name
Rule: Always set explicit related_name for reverse relationships.
✅ Good:
class User(models.Model):
username = models.CharField(max_length=150)
class Post(models.Model):
author = models.ForeignKey(
User,
related_name='posts', # user.posts.all()
related_query_name='post', # User.objects.filter(post__title='...')
)
title = models.CharField(max_length=200)
class Comment(models.Model):
post = models.ForeignKey(
Post,
related_name='comments',
related_query_name='comment',
)
author = models.ForeignKey(
User,
related_name='comments',
related_query_name='comment',
)
content = models.TextField()
# Usage:
user = User.objects.get(username='john')
user_posts = user.posts.all() # Uses related_name
user_comments = user.comments.all()
# Query filtering uses related_query_name
users_with_python_posts = User.objects.filter(post__title__contains='Python')
posts_with_comments = Post.objects.filter(comment__isnull=False)
❌ Bad:
class Post(models.Model):
# Bad: No related_name, Django generates 'post_set'
author = models.ForeignKey(User,
# Unclear usage:
user_posts = user.post_set.all() # What is post_set?
Related Name Best Practices:
- Use plural for one-to-many:
related_name='posts' - Use singular for one-to-one:
related_name='profile' - Use
related_query_namefor clear query filtering - Use
related_name='+'to disable reverse relation if not needed - Avoid name conflicts across apps using
app_label
2.7 Database Indexing Strategies
Rule: Add indexes for fields frequently used in queries, filtering, and ordering.
✅ Good Indexing:
class Article(models.Model):
"""Article model with strategic indexing."""
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True) # Unique creates index automatically
author = models.ForeignKey(User, # FK creates index
category = models.ForeignKey(Category, null=True)
status = models.CharField(max_length=20, choices=StatusChoices.choices)
featured = models.BooleanField(default=False)
published_at = models.DateTimeField(null=True, blank=True)
view_count = models.PositiveIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
# Index for filtering by status and ordering
models.Index(fields=['status', '-published_at'], name='status_pub_idx'),
# Index for author's articles
models.Index(fields=['author', '-created_at'], name='author_articles_idx'),
# Index for category browsing
models.Index(fields=['category', '-published_at'], name='cat_pub_idx'),
# Index for featured articles
models.Index(
fields=['-published_at'],
condition=models.Q(featured=True),
name='featured_idx', # Partial index (PostgreSQL)
),
# Compound index for complex queries
models.Index(
fields=['status', 'featured', '-view_count'],
name='popular_published_idx',
),
]
# Text search index (PostgreSQL)
# For full-text search capabilities
# Note: Requires GinIndex from django.contrib.postgres.indexes
When to Add Indexes:
- Foreign keys (automatic in Django)
- Fields in
WHEREclauses - Fields in
ORDER BYclauses - Fields in
JOINconditions - Unique fields (automatic)
- Fields used in
filter(),exclude(),get()
When NOT to Add Indexes:
- Small tables (< 10,000 rows)
- Fields that change frequently
- Fields with low cardinality (few unique values like boolean)
- Too many indexes slow down writes
Index Analysis:
# Check if query uses indexes
from django.db import connection
from django.test.utils import CaptureQueriesContext
with CaptureQueriesContext(connection) as queries:
articles = Article.objects.filter(
status='PUBLISHED',
featured=True
).order_by('-view_count')[:10]
list(articles) # Force evaluation
for query in queries:
print(query['sql'])
# Check EXPLAIN output in database
2.8 Migration Best Practices
Rule: Create focused, reviewable migrations and handle data migrations separately.
✅ Good Migration Practices:
1. Small, Focused Migrations:
# Create separate migrations for different changes
python manage.py makemigrations --name add_email_verified_field
python manage.py makemigrations --name add_user_role_choices
2. Data Migration Example:
# Generated migration file: 0003_populate_user_roles.py
from django.db import migrations
def populate_user_roles(apps, schema_editor):
"""Populate user roles based on is_staff and is_superuser."""
User = apps.get_model('users', 'User')
# Update in batches for large datasets
User.objects.filter(is_superuser=True).update(role='ADMIN')
User.objects.filter(is_staff=True, is_superuser=False).update(role='MOD')
User.objects.filter(is_staff=False, is_superuser=False).update(role='USER')
def reverse_populate_user_roles(apps, schema_editor):
"""Reverse operation."""
User = apps.get_model('users', 'User')
User.objects.all().update(role='USER')
class Migration(migrations.Migration):
dependencies = [
('users', '0002_user_role'),
]
operations = [
migrations.RunPython(
populate_user_roles,
reverse_code=reverse_populate_user_roles,
),
]
3. Safe Schema Changes:
# Safe: Adding nullable field
class Migration(migrations.Migration):
operations = [
migrations.AddField(
model_name='user',
name='phone_number',
field=models.CharField(max_length=20, null=True, blank=True),
),
]
# Safe: Adding field with default
class Migration(migrations.Migration):
operations = [
migrations.AddField(
model_name='post',
name='view_count',
field=models.PositiveIntegerField(default=0),
),
]
4. Multi-Step Migrations for Removing Fields:
# Step 1: Make field nullable (deploy)
field=models.CharField(max_length=100, null=True, blank=True)
# Step 2: Remove from code, create migration (deploy)
# Step 3: Drop column (deploy)
Migration Best Practices:
- Review generated migrations before committing
- Use descriptive migration names (
--name) - Never edit applied migrations in production
- Use
RunPythonfor data migrations with reverse operations - Test migrations on production-like data
- Use
--checkin CI to detect missing migrations - Squash old migrations when they pile up
- Handle large datasets with batching in data migrations
- Add indexes in separate migrations (can be slow)
- Use database transactions (default in Django)
Task 3: Views and URLs Best Practices
3.1 Class-Based Views (CBVs) vs Function-Based Views (FBVs)
Rule: Use CBVs for CRUD operations, FBVs for simple or unique logic.
✅ Good - Use CBVs for Standard CRUD:
"""Blog views using class-based views."""
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.urls import reverse_lazy
from .models import Post
from .forms import PostForm
class PostListView(ListView):
"""List all published posts."""
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 20
def get_queryset(self):
"""Return only published posts with author info."""
return Post.objects.published().with_author_info()
def get_context_data(self, **kwargs):
"""Add additional context data."""
context = super().get_context_data(**kwargs)
context['featured_posts'] = Post.objects.featured()[:5]
return context
class PostDetailView(DetailView):
"""Display single post."""
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
def get_object(self, queryset=None):
"""Get post and increment view count."""
post = super().get_object(queryset)
post.increment_view_count()
return post
class PostCreateView(LoginRequiredMixin, CreateView):
"""Create new post."""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
success_url = reverse_lazy('blog:post-list')
def form_valid(self, form):
"""Set author to current user."""
form.instance.author = self.request.user
return super().form_valid(form)
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
"""Update existing post."""
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def test_func(self):
"""Check if user is author or admin."""
post = self.get_object()
return self.request.user == post.author or self.request.user.is_staff
def get_success_url(self):
"""Redirect to post detail."""
return self.object.get_absolute_url()
class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
"""Delete post."""
model = Post
template_name = 'blog/post_confirm_delete.html'
success_url = reverse_lazy('blog:post-list')
def test_func(self):
"""Check if user is author or admin."""
post = self.get_object()
return self.request.user == post.author or self.request.user.is_staff
✅ Good - Use FBVs for Unique Logic:
"""Blog views using function-based views for custom logic."""
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from .models import Post
@login_required
@require_POST
def toggle_post_featured(request, pk):
"""Toggle post featured status (unique logic, FBV appropriate)."""
post = get_object_or_404(Post, pk=pk)
# Check permissions
if not request.user.is_staff:
return JsonResponse({'error': 'Permission denied'}, status=403)
# Toggle featured status
post.featured = not post.featured
post.save(update_fields=['featured'])
return JsonResponse({
'success': True,
'featured': post.featured,
})
def search_posts(request):
"""Search posts (custom search logic, FBV appropriate)."""
query = request.GET.get('q', '')
category = request.GET.get('category', '')
posts = Post.objects.published()
if query:
posts = posts.filter(
models.Q(title__icontains=query) |
models.Q(content__icontains=query)
)
if category:
posts = posts.filter(category__slug=category)
context = {
'posts': posts,
'query': query,
'category': category,
}
return render(request, 'blog/search_results.html', context)
When to Use Each:
Use CBVs when:
- Standard CRUD operations
- Need inheritance and mixins
- Working with forms
- Need consistent structure across views
Use FBVs when:
- Simple logic
- Unique business logic that doesn't fit CBV pattern
- API endpoints with custom logic
- Complex multi-step flows
- Cleare
…(truncated)