Create some test data

Django is a powerful web framework that enables developers to quickly build robust, database-driven applications. Out of the box, Django provides a solid foundation and includes many batteries-included features. However, there are additional best practices and optimizations that can take your Django projects to the next level in terms of maintainability, performance, and security.

As an experienced Django developer, I‘ve learned many valuable lessons over the years building real-world applications. In this article, I‘ll share some of the most impactful best practices I recommend for structuring and scaling Django projects. Whether you‘re just getting started with Django or maintaining a large production application, these tips will help you write cleaner, more efficient, and more maintainable code. Let‘s dive in!

1. Use Virtual Environments

One of the first things you should do when starting any Python project is to create a virtual environment. Virtual environments allow you to isolate the dependencies for a given project, avoiding conflicts with other projects or system-wide packages.

To create a virtual environment, make sure you have virtualenv installed:

$ pip install virtualenv

Then create and activate a new virtual environment for your project:

$ virtualenv myproject
$ source myproject/bin/activate 

Once activated, any packages you install using pip will be isolated to this environment. You can deactivate the virtualenv at any time by running:

$ deactivate

I recommend including a requirements.txt file in your project that specifies the exact dependencies, making it easy for other developers to reproduce the environment:

Django==3.2
psycopg2==2.9.1
Pillow==8.3.1

Then anyone can install the required packages using:

$ pip install -r requirements.txt

2. Configure Settings for Different Environments

Another best practice is to use separate settings files for different environments, such as local development, staging, and production. This allows you to have different configurations, like database settings and debug mode, for each environment.

A typical project structure would look like:

myproject/
    manage.py
    myproject/
        __init__.py
        settings/
            __init__.py 
            base.py
            local.py
            staging.py 
            production.py
        urls.py
        wsgi.py
    requirements.txt        

The base.py file contains common settings used across all environments. The local.py, staging.py and production.py files inherit from base.py and can override any settings as needed for that environment.

For example, base.py may contain:

SECRET_KEY = os.environ[‘SECRET_KEY‘] 
DEBUG = False
ALLOWED_HOSTS = []

While production.py contains:

  
from .base import *

DEBUG = False
ALLOWED_HOSTS = [‘myproject.com‘] DATABASES = { ‘default‘: { ‘ENGINE‘: ‘django.db.backends.postgresql‘, ‘NAME‘: ‘myproject‘, ‘USER‘: ‘myprojectuser‘, ‘PASSWORD‘: os.environ[‘DB_PASSWORD‘], } }

To specify which settings file to use, you can modify the manage.py file:

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings.local")  

3. Follow Conventions for Project Structure

Django provides flexibility in how you structure your projects, but following some conventions can help keep things organized and maintainable, especially as projects grow.

Some recommendations:

  • Create separate apps for discrete parts of your project functionality
  • Keep view logic out of models and forms
  • Use custom template tags and filters to encapsulate presentation logic
  • Prefix template, static, and url names with the app name to avoid collisions

For example, a social media project may have apps for posts, profiles, and notifications:

  
myproject/
    posts/
        templates/
            posts/
                create.html
                detail.html
                list.html
        urls.py
        views.py
        models.py  
    profiles/
        ...
    notifications/ 
        ...

URLs in posts/urls.py would look like:

urlpatterns = [
    path(‘‘, views.post_list, name=‘posts_list‘),
    path(‘create/‘, views.post_create, name=‘posts_create‘),  
    path(‘<int:id>/‘, views.post_detail, name=‘posts_detail‘),
]  

4. Optimize Database Queries

Django‘s ORM provides a convenient way to interact with your database, but it‘s easy to introduce inefficiencies that lead to slow queries, especially as your data grows. Optimizing queries can dramatically speed up your application.

Some tips:

  • Use select_related and prefetch_related when accessing related objects to minimize queries
  • Use queryset.explain() to see how a query will be executed by the database
  • Avoid queries in loops, use bulk operations like bulk_create instead
  • Add indexes for commonly filtered/sorted fields
  • Denormalize data that is frequently accessed together

For example, imagine a view that gets a list of books with their authors:

def book_list(request):
    books = Book.objects.all()
    return render(request, ‘book_list.html‘, {‘books‘: books}) 

And using it in the template:

<ul>
{% for book in books %}
    <li>{{ book.title }} by {{ book.author.name }}</li>  
{% endfor %}
</ul>

This will result in 1 query to get the books, and then N queries to get each book‘s author, known as the "N+1 problem". We can fix this by using select_related:

def book_list(request):
    books = Book.objects.all().select_related(‘author‘)
    return render(request, ‘book_list.html‘, {‘books‘: books})

Now only 1 query is needed to get the books and their authors. Examining the generated queries using queryset.explain() can help uncover other optimizations.

5. Implement Security Best Practices

Security is critical for any web application. Django provides many built-in protections, but there are additional steps you should take to harden your app:

  • Always use HTTPS in production
  • Enable HSTS headers to enforce HTTPS
  • Use Django‘s CSRF protection middleware
  • Set a custom User model to use stronger password hashing like PBKDF2
  • Validate and sanitize all user input, use Django forms
  • Avoid using the safe string template tag, which can introduce XSS vulnerabilities
  • Limit access to admin and authenticate pages
  • Keep Django and dependencies updated to latest security patches

Many of these are easy to implement. For example, to enable HSTS headers, simply add to your settings file:

SECURE_HSTS_SECONDS = 31536000 # 1 year  
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

And to use the PBKDF2 password hasher:

  
PASSWORD_HASHERS = [
    ‘django.contrib.auth.hashers.PBKDF2PasswordHasher‘,
    ‘django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher‘,
]

6. Write Tests

Automated tests are essential for maintaining a high-quality Django project. They provide a safety net, ensuring that changes don‘t break existing functionality. Django‘s testing framework makes it easy to write unit and integration tests.

Best practices include:

  • Write tests for all new functionality
  • Aim for high code coverage
  • Use factories like Factory Boy to generate test data
  • Run tests automatically using continuous integration
  • Separate unit vs integration tests

For example, here‘s a simple unit test for a utility function:

from django.test import TestCase

class UtilsTestCase(TestCase): def test_slugify(self): self.assertEqual(slugify(‘Hello World!‘), ‘hello-world‘) self.assertEqual(slugify(‘This is a test‘), ‘this-is-a-test‘)

And an integration test for a view:

from django.test import TestCase

class PostViewTestCase(TestCase): def test_post_list(self):

    Post.objects.create(title="Post 1", body="body 1")
    Post.objects.create(title="Post 2", body="body 2")

    response = self.client.get(reverse(‘post_list‘))
    self.assertEqual(response.status_code, 200)
    self.assertContains(response, "Post 1")  
    self.assertContains(response, "Post 2")

Running tests regularly and aiming for high code coverage improves code quality and maintains it over time. Many Django packages also include testing utilities to simplify more complex testing needs.

7. Deployment Best Practices

Deploying Django applications can be complex, especially when dealing with dependencies, database migrations, static files, etc. Using a systematic, repeatable deployment process is key.

Some recommendations:

  • Use tools like Docker to ensure parity between development and production environments
  • Automate deployment with continuous delivery pipelines
  • Don‘t run Django in debug mode in production
  • Use WSGI servers like Gunicorn or uWSGI in production, not Django‘s runserver
  • Host static files separately using a CDN or file storage service like S3
  • Use environment variables for secrets and configuration, don‘t hardcode in settings files

With Docker, you define the runtime environment for the application, including OS, Python version, packages, etc. in a Dockerfile:

  
FROM python:3.8
ENV PYTHONUNBUFFERED 1
RUN mkdir /code  
WORKDIR /code
COPY requirements.txt /code/
RUN pip install -r requirements.txt
COPY . /code/  

And a docker-compose.yml file that specifies the services, like the database:

version: ‘3‘

services: db: image: postgres web: build: . command: python manage.py runserver 0.0.0.0:8000 environment:

  • SECRET_KEY=abcdefg123456
  • DB_PASSWORD=mysecretpassword volumes:
  • .:/code
    ports:
  • "8000:8000" depends_on:
  • db

With this setup, running the application is as simple as docker-compose up, and the environment is consistent across every machine. Deployment can be done by building the Docker image and pushing it to a container registry.

8. Monitoring & Logging

Once your application is deployed and running in production, it‘s important to have monitoring and logging set up to detect and diagnose any issues that may come up.

Some best practices:

  • Use a logging framework like Python‘s built-in logging module
  • Log at the appropriate level – debug for development, info/warning/error for production
  • Include key information like user IDs, request IDs for traceability
  • Integrate with a monitoring service like Sentry or Rollbar to detect and alert on exceptions
  • Use Django‘s middleware for request/response logging
  • Monitor key application metrics like response times, database query performance, etc.

Django‘s logging configuration allows granular control over what gets logged and where:

LOGGING = {
    ‘version‘: 1,
    ‘disable_existing_loggers‘: False,
    ‘handlers‘: {
        ‘console‘: {
            ‘class‘: ‘logging.StreamHandler‘,  
        },
    },
    ‘loggers‘: {
        ‘django‘: {
            ‘handlers‘: [‘console‘],
            ‘level‘: os.getenv(‘DJANGO_LOG_LEVEL‘, ‘INFO‘),
        },
    },
}  

Integrating a tool like Sentry is as simple as installing the Sentry SDK and adding it to your Django applications:

import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration

sentry_sdk.init( dsn="your-sentry-dsn", integrations=[DjangoIntegration()], )

With this, exceptions will automatically be logged to Sentry where you can see details like stack traces, affected users, frequency, and more. This is invaluable for identifying and fixing bugs quickly.

Monitoring performance of key pages and background jobs proactively with a tool like Sentry or New Relic can also help identify issues before they impact users.

Conclusion

Building and maintaining high-quality Django applications requires following best practices throughout the development lifecycle, from project setup to deployment and monitoring. In this article we covered some of the most impactful techniques:

  • Using virtual environments for dependency isolation
  • Configuring settings for different environments
  • Following conventions for project layout
  • Optimizing database queries
  • Implementing security best practices
  • Writing automated tests
  • Using Docker for deployment
  • Setting up logging and monitoring

Adopting these practices will lead to Django projects that are more robust, performant and maintainable. They require some additional up-front effort but pay dividends in the long run. Do you have any other Django best practices you‘ve found useful in your own projects? Let me know in the comments!

Similar Posts