Django Development: 14 Best Practices You Should Always Follow!

Follow these essential Django development tips to write cleaner, faster, and more maintainable code.

Django Development: 14 Best Practices You Should Always Follow!
Photo by Kevin Bosc on Unsplash

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

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.