Django Development: 14 Best Practices You Should Always Follow!
Follow these essential Django development tips to write cleaner, faster, and more maintainable code.

BUILD BETTER DJANGO APPS WITH THESE PROVEN BEST PRACTICES!
Django Development: 14 Best Practices You Should Always Follow!
Hey everyone! I’ve been working with Django for a long time, and along the way, I’ve adopted several best practices that have not only improved my projects but also helped me write cleaner, faster, and more maintainable code.
I am going to reveal these 14 best practice that you should always follow when you going to start with your Django project.
1. Follow the DRY Principle (Don’t Repeat Yourself)
This is most common principle, that you should always apply in your codebase. This avoid you to write duplicate code, If you find yourself repeating similar code in different places, refactor it into reusable functions, classes, or mixins.
Example:
Instead of repeating a function across multiple views, create a utility function:
# utils.py
def send_welcome_email(user):
subject = "Welcome to Our Platform"
message = f"Hi {user.username}, welcome!"
user.email_user(subject, message)
# views.py
from .utils import send_welcome_email
def register_user(request):
user = User.objects.create(username="Aashish", email="aashish@example.com")
send_welcome_email(user)
return HttpResponse("User registered!")
In this example, we’ve created the send_welcome_email
function to send a welcome email. Whenever we need to send a welcome email to a user, we can simply reuse this function.
2. Use Django’s ORM Efficiently
Django ORM is powerful, but your inefficient queries can slow down performance sometime. To avoid these performance issues you can write your ORM queries efficiently.
Best Practices for Optimizing Django ORM Queries
1. Use select_related
and prefetch_related (
Avoid N+1 Query Problem)
Django provides select_related
and prefetch_related
to optimize queries involving related objects.
Example Without Optimization (N+1 Problem)
# This will trigger multiple queries for each author
books = Book.objects.all()
for book in books:
print(book.author.name) # non-optimize query (N+1 problem)
select_related
: Uses SQL joins to retrieve related data in a single query (recommended for ForeignKey relationships).
books = Book.objects.select_related('author').all()
for book in books:
print(book.author.name) # Fetches data in a single query
prefetch_related
: Fetches related objects separately and then links them in Python (useful for ManyToMany relationships).
books = Book.objects.prefetch_related('categories').all()
for book in books:
print(book.categories.all()) # Optimized ManyToMany retrieval
2. Use Indexing for Faster Lookups
Indexes improve database performance by speeding up lookups. Add indexes to frequently queried fields.
from django.db import models
class User(models.Model):
email = models.EmailField(unique=True, db_index=True) # Adding index
3. Use only()
and defer()
to Limit Data Loading
When querying large models, avoid loading unnecessary fields.
Example: Fetching Only Required Fields
users = User.objects.only("name", "email") # Loads only name and email fields
Example: Deferring Unnecessary Fields
users = User.objects.defer("profile_picture", "bio") # Skips loading large fields
3. Follow the ‘Fat Model, Thin View’ Principle
Keep business logic inside models instead of views to keep views clean. Use model methods to encapsulate logic.
# models.py
class Order(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
total_price = models.DecimalField(max_digits=10, decimal_places=2)
def apply_discount(self, percentage):
self.total_price *= (1 - percentage / 100)
self.save()
# views.py
def apply_order_discount(request, order_id):
order = Order.objects.get(id=order_id)
order.apply_discount(10) # call the logic
return HttpResponse(f"New total: {order.total_price}")
4. Separate Settings for Development and Production
Managing settings in a Django project can get messy easily, especially as your project scales. A single settings.py file often turns into a bloated, hard-to-maintain mess.
By following these steps you can easily split your Django settings with multiple environments.
1. Create a settings/
Directory
Inside your Django project, replace settings.py
with a new folder:
mv myproject/settings.py myproject/settings/
mkdir myproject/settings
touch myproject/settings/__init__.py
by following this CLI or you can directly do it in code editor.
2. Create Environment-Specific Settings Files
Inside the settings/
directory, create the following files:
base.py
→ Common settings for all environments.development.py
→ Settings for local development.production.py
→ Settings for live deployment.staging.py
→ Settings for staging (if needed).
touch myproject/settings/base.py
touch myproject/settings/development.py
touch myproject/settings/production.py
3. Define Common Settings in base.py
# settings/base.py
from pathlib import Path
import os
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "your-default-secret-key")
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {"context_processors": ["django.template.context_processors.request"]},
}
]
STATIC_URL = "/static/"
4. Define Environment-Specific Settings
development.py
– Debug Mode ON
# settings/development.py
from .base import * # this will import all the common settings
DEBUG = True # Debug mode is True in development
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
# Sqlite3 database for development
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Email settings for development
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
production.py
– Debug Mode OFF
# settings/production.py
from .base import * # this will import all the common settings
DEBUG = False # Debug mode is False in production
ALLOWED_HOSTS = ["yourdomain.com"]
# Postgres database for production
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.getenv("DB_NAME"),
"USER": os.getenv("DB_USER"),
"PASSWORD": os.getenv("DB_PASSWORD"),
"HOST": os.getenv("DB_HOST"),
"PORT": os.getenv("DB_PORT", "5432"),
}
}
# Email settings for production
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = os.getenv("EMAIL_HOST")
EMAIL_PORT = os.getenv("EMAIL_PORT")
EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD")
5. Set the Environment Variable for DJANGO_SETTINGS_MODULE
Now, tell Django which settings file to use based on the environment.
For Development:
On the production server, add this in .env file:
DJANGO_SETTINGS_MODULE=myproject.settings.development
For Production:
On the production server add this in .env file:
DJANGO_SETTINGS_MODULE=myproject.settings.production
Or you can set it in wsgi.py
or asgi.py
:
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings.production")
5. Use Django’s Built-in Features
Django provides many built-in tools that save development time.
Example: Instead of manually handling authentication, use Django’s login_required decorator for authentication system:
from django.contrib.auth.decorators import login_required
@login_required
def profile(request):
return HttpResponse("Welcome to your profile")
6. Implement Middleware Judiciously
Use middleware for cross-cutting concerns (such as authentication, logging, security, etc.), adding too many unnecessary middleware classes can slow down your application.
Example:
Create a middleware to log request processing time.
# myapp/middleware.py
import time
class LogExecutionTimeMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
start_time = time.time()
response = self.get_response(request)
execution_time = time.time() - start_time
print(f"Request took {execution_time:.2f} seconds")
return response
Then, register it in settings.py
:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.middleware.common.CommonMiddleware',
'myapp.middleware.LogExecutionTimeMiddleware', # Custom middleware
]
Bad Middleware Example
A middleware that queries the database on every request (not efficient):
from django.utils.deprecation import MiddlewareMixin
from myapp.models import UserActivityLog
class BadMiddleware(MiddlewareMixin):
def process_request(self, request):
UserActivityLog.objects.create(user=request.user, path=request.path) # Database call on every request
Instead, this should be handled asynchronously via Celery or inside a view.
7. Write Tests
Django comes with a built-in testing framework that helps ensure your code is reliable and works as expected. Writing tests is crucial for catching bugs early, improving code quality, and making refactoring safer.
Example:
from django.test import TestCase
from .models import Order
class OrderTestCase(TestCase):
def test_apply_discount(self):
order = Order.objects.create(total_price=100)
order.apply_discount(10)
self.assertEqual(order.total_price, 90)
8. Version Control with Git in Django
Using Git for version control in Django (or any project) is essential for tracking changes, collaborating efficiently, and maintaining a clean codebase.
Basic Git Setup for a Django Project
Initialize Git
Run this in your project directory:
git init
This creates a .git
folder to track changes.
Create a .gitignore
File
Add a .gitignore
file to prevent committing unnecessary files:
Example .gitignore
for Django:
# Python-related
*.pyc
__pycache__/
venv/
.env
# Django-related
db.sqlite3
/media/
staticfiles/
# IDE & OS-specific
.vscode/
.idea/
.DS_Store
Commit the .gitignore
file:
git add .gitignore
git commit -m "Added .gitignore"
Commit Changes Regularly
After making changes:
git add .
git commit -m "Added new feature"
Write meaningful commit messages.
Use Branching for Features
Create a new branch before working on a feature:
git checkout -b feature-authentication
After implementing the feature merge with main branch:
git add .
git commit -m "Implemented authentication system"
git checkout main
git merge feature-authentication
Then delete the branch:
git branch -d feature-authentication
Connect to Remote Repository (GitHub, GitLab, Bitbucket)
Create a repository on GitHub and add the remote link:
git remote add origin https://github.com/yourusername/repository-name.git
git push -u origin main
Pull Latest Changes Before Pushing
Before pushing your code:
git pull origin main --rebase
This prevents conflicts and keeps your local repo up to date.
9. Keep Dependencies Up-to-Date
Use pip freeze
to check outdated dependencies and update them.
Check Installed Packages
To see the currently installed packages and versions:
pip freeze
Save Dependencies to requirements.txt
To create a requirements.txt
file:
pip freeze > requirements.txt
This ensures that anyone working on the project installs the same versions.
Update All Dependencies
To upgrade all packages to their latest compatible versions:
pip install --upgrade -r requirements.txt
Check for Outdated Packages
To see which dependencies are outdated:
pip list --outdated
Example output:
Package Version Latest Type
-------------- ------- ------ -----
Django 4.2.1 5.1.7 wheel
requests 2.26.0 2.31.0 wheel
To update a specific package:
pip install --upgrade Django
10. Use Django’s Template System Wisely
Placing complex logic inside templates makes them harder to read, maintain, and debug.
Example:
Instead of this in the template:
{% if user.is_staff and user.last_login and user.last_login.year == 2024 %}
<p>Welcome back, Admin!</p>
{% endif %}
Move logic to views.py
:
def admin_welcome_message(user):
return user.is_staff and user.last_login and user.last_login.year == 2024
And use it in the template:
{% if admin_welcome_message %}
<p>Welcome back, Admin!</p>
{% endif %}
11. Handle Static and Media Files
Django provides a built-in way to manage static files (CSS, JavaScript, images) and media files (user-uploaded content like profile pictures, documents, etc.).
Configuring Static Files
Set Up Static Settings in settings.py
Add the following to your settings.py file:
# settings.py
import os
STATIC_URL = '/static/' # URL to access static files
# Collect all static files in one place when using `collectstatic`
STATIC_ROOT = BASE_DIR/'staticfiles'
# Tell Django where to find static files in each app
STATICFILES_DIRS = BASE_DIR/'static'
Organizing Static Files
Django looks for static files inside each app’s static/
folder.
Example Project Structure:
myproject/
│── myapp/
│ ├── static/
│ │ ├── myapp/
│ │ │ ├── css/style.css
│ │ │ ├── js/script.js
│ │ │ ├── images/logo.png
│── static/ # (if using STATICFILES_DIRS)
│── templates/
│── settings.py
Using Static Files in Templates
Load static
in the template:
{% load static %}
<link rel="stylesheet" href="{% static 'myapp/css/style.css' %}">
<img src="{% static 'myapp/images/logo.png' %}" alt="Logo">
Collect Static Files (For Deployment)
Before deploying, run:
python manage.py collectstatic
This gathers all static files into STATIC_ROOT
.
For production, serve static files using WhiteNoise (if using Gunicorn):
pip install whitenoise
Update MIDDLEWARE
in settings.py
:
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # Add this
...
]
Configuring Media Files (User Uploads)
Set Up Media Settings in settings.py
# settings.py
MEDIA_URL = '/media/' # URL to access media files
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # Directory to store uploads
Organizing Media Files
Example Structure for media files:
myproject/
│── media/ # User-uploaded files will be stored here
│ ├── profile_pics/
│ ├── documents/
│── myapp/
│ ├── models.py
│── settings.py
Handling File Uploads in Models
Define a model with an ImageField
or FileField
:
# models.py
from django.db import models
class Profile(models.Model):
name = models.CharField(max_length=100)
profile_pic = models.ImageField(upload_to='profile_pics/') # Stores in media/profile_pics/
Handling File Uploads in Forms
# forms.py
from django import forms
from .models import Profile
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = ['name', 'profile_pic']
Handling File Uploads in Views
# views.py
from django.shortcuts import render
from .forms import ProfileForm
def upload_profile(request):
if request.method == 'POST':
form = ProfileForm(request.POST, request.FILES)
if form.is_valid():
form.save()
else:
form = ProfileForm()
return render(request, 'upload.html', {'form': form})
Handling File Uploads in Templates
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Upload</button>
</form>
Serving Media Files in Development
During development, add this to urls.py
:
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
# Other URLs...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
In production, use cloud storage (Amazon S3, Google Cloud Storage) instead of MEDIA_ROOT
.
12. Secure Your Application
Django provides built-in security features to protect against common vulnerabilities. Below are key best practices to secure your Django app.
Enforce HTTPS (SSL/TLS)
Always redirect HTTP to HTTPS to prevent man-in-the-middle (MITM) attacks.
Add the following settings in settings.py
:
# settings.py
SECURE_SSL_REDIRECT = True # Redirects HTTP to HTTPS
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # Required if using a proxy/load balancer
If you’re using Cloudflare, Nginx, or AWS Load Balancer, configure SSL there as well.
Secure Cookies (CSRF & Session Security)
Protect cookies from interception and session hijacking.
# settings.py
CSRF_COOKIE_SECURE = True # Prevents CSRF cookie from being sent over HTTP
SESSION_COOKIE_SECURE = True # Ensures session cookie is only sent over HTTPS
CSRF_COOKIE_HTTPONLY = True # Prevents JavaScript from accessing the CSRF cookie
SESSION_COOKIE_HTTPONLY = True # Prevents JavaScript from accessing the session cookie
Prevent Clickjacking
Clickjacking occurs when a malicious site embeds your website inside an <iframe>
, tricking users into clicking hidden elements.
# settings.py
X_FRAME_OPTIONS = "DENY" # Blocks embedding your site inside iframes
Use Content Security Policy (CSP)
CSP helps prevent Cross-Site Scripting (XSS) by controlling which sources are allowed to load scripts, styles, and images.
Install django-csp
pip install django-csp
Add CSP Middleware in settings.py
# settings.py
MIDDLEWARE += ["csp.middleware.CSPMiddleware"]
CSP_DEFAULT_SRC = ("'self'",) # Only allow resources from your domain
CSP_SCRIPT_SRC = ("'self'", "cdnjs.cloudflare.com") # Allow scripts from CDN
CSP_STYLE_SRC = ("'self'", "fonts.googleapis.com") # Allow styles from Google Fonts
Secure HTTP Headers
Django provides default security headers, but you should enforce stricter policies.
# settings.py
SECURE_HSTS_SECONDS = 31536000 # Forces HTTPS for 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True # Applies to all subdomains
SECURE_HSTS_PRELOAD = True # Enables browser preload list
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin" # Protects referrer data
Disable Debug Mode in Production
Never expose sensitive information in production.
# settings.py
DEBUG = False
ALLOWED_HOSTS = ["yourdomain.com"]
Restrict Admin Panel Access
Django’s admin panel is a high-value target for attackers.
Change the Default Admin URL
# urls.py
urlpatterns = [
path("secure-admin/", admin.site.urls), # Avoid using "/admin/"
]
Protect Against SQL Injection
Use Django’s ORM instead of raw SQL queries.
user = User.objects.filter(username=username).first()
13. Document Your Code
Writing clear docstrings and comments improves code readability, maintainability, and collaboration.
Example:
def calculate_total_price(price, discount):
"""
Calculate total price after applying discount.
:param price: Original price
:param discount: Discount percentage
:return: Final price after discount
"""
return price * (1 - discount / 100)
14. Monitor and Optimize Performance
Django applications can become slow due to inefficient database queries, unoptimized templates, or excessive middleware usage. To analyze and optimize performance, tools like Django Debug Toolbar help identify bottlenecks.
Install Django Debug Toolbar
Run the following command:
pip install django-debug-toolbar
Configure settings.py
# settings.py
INSTALLED_APPS += ["debug_toolbar"]
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]
# Define internal IPs (required for the toolbar to appear in development):
INTERNAL_IPS = [
"127.0.0.1",
]
Update urls.py
Modify the urls.py
file to include the debug toolbar in development:
# urls.py
from django.conf import settings
from django.conf.urls import include
from django.urls import path
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
path("__debug__/", include(debug_toolbar.urls)),
] + urlpatterns
Now, run the server and check the toolbar on the right side of your app!
Conclusion
By following these best practices, you can build maintainable, scalable, and secure Django applications. Stay updated with Django’s latest releases and community recommendations to utlizing new features and improvements.