Welcome to this comprehensive guide on building modern websites using Django 5.0 and Python 3.12. This tutorial will walk you through everything you need to know to create robust, scalable web applications using the latest features and best practices. Whether you’re a beginner or an experienced developer, this guide will help you understand the fundamentals and advanced concepts of Django web development.
Setting Up Your Development Environment
A proper development environment is crucial for efficient Django development. We’ll use Python 3.12 for its improved performance and new features like better error messages, improved type hints, and faster startup times.
Installing Python 3.12 and Django 5.0
First, download Python 3.12 from the official Python website. After installation, verify your Python version:
python --version # Should output: Python 3.12.x
It’s essential to use virtual environments to isolate project dependencies. Here’s how to set one up:
# On Windows python -m venv mywebsite_env mywebsite_env\Scripts\activate # On macOS/Linux python3 -m venv mywebsite_env source mywebsite_env/bin/activate
Install Django 5.0 and other essential packages:
pip install django==5.0 pip install python-dotenv pip install django-debug-toolbar pip install django-compressor pip install pillow pip install django-allauth pip install django-crispy-forms
Create a requirements.txt file:
pip freeze > requirements.txt
Development Tools Setup
For optimal development, install these VS Code extensions:
- Python
- Django
- Pylance
- Git Lens
- Python Test Explorer
Configure VS Code settings.json for Django:
{ "python.linting.pylintEnabled": true, "python.linting.enabled": true, "python.formatting.provider": "black", "editor.formatOnSave": true, "files.associations": { "**/*.html": "html", "**/templates/**/*.html": "django-html", "**/templates/**/*": "django-txt" } }
Django Project Structure and Best Practices 2024
Creating a well-organized project structure is crucial for maintainability. Let’s create a new Django project with a modern structure:
django-admin startproject mywebsite cd mywebsite python manage.py startapp core python manage.py startapp accounts python manage.py startapp api
Advanced Project Structure:
mywebsite/ ├── mywebsite/ │ ├── __init__.py │ ├── settings/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── development.py │ │ ├── production.py │ │ └── staging.py │ ├── urls.py │ ├── asgi.py │ └── wsgi.py ├── apps/ │ ├── core/ │ ├── accounts/ │ └── api/ ├── templates/ │ ├── base.html │ ├── components/ │ └── pages/ ├── static/ │ ├── css/ │ ├── js/ │ └── images/ ├── media/ ├── docs/ ├── tests/ │ ├── integration/ │ └── unit/ ├── requirements/ │ ├── base.txt │ ├── development.txt │ └── production.txt ├── scripts/ │ ├── deploy.sh │ └── setup.sh ├── .env.example ├── .gitignore ├── README.md └── manage.py
Comprehensive Settings Configuration
Create a modular settings structure (in settings/base.py):
from pathlib import Path from dotenv import load_dotenv import os # Load environment variables load_dotenv() BASE_DIR = Path(__file__).resolve().parent.parent.parent SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # Third-party apps 'debug_toolbar', 'compressor', 'allauth', 'allauth.account', 'crispy_forms', 'rest_framework', # Local apps 'apps.core.apps.CoreConfig', 'apps.accounts.apps.AccountsConfig', 'apps.api.apps.ApiConfig', ] 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', 'debug_toolbar.middleware.DebugToolbarMiddleware', ] TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] # Database configuration with environment variables 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', 'localhost'), 'PORT': os.getenv('DB_PORT', '5432'), } }
Leveraging Django’s Latest Features
Advanced Async Views
Django 5.0 brings improved async support. Here’s a comprehensive example:
# views.py import asyncio from django.http import JsonResponse from django.views.decorators.http import require_http_methods from django.core.cache import cache from typing import Dict, Any async def fetch_external_api_data(url: str) -> Dict[str, Any]: async with httpx.AsyncClient() as client: response = await client.get(url) return response.json() async def get_cached_data(key: str) -> Dict[str, Any]: if cached_data := await cache.aget(key): return cached_data return None @require_http_methods(["GET"]) async def async_dashboard(request): # Parallel async operations user_data = asyncio.create_task(get_user_data(request.user.id)) analytics = asyncio.create_task(fetch_external_api_data(ANALYTICS_API_URL)) notifications = asyncio.create_task(get_user_notifications(request.user.id)) # Wait for all tasks to complete dashboard_data = await asyncio.gather( user_data, analytics, notifications ) return JsonResponse({ 'user_data': dashboard_data[0], 'analytics': dashboard_data[1], 'notifications': dashboard_data[2] })
Advanced HTMX Integration
Create a more sophisticated HTMX implementation:
<!-- templates/base.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}{% endblock %}</title> <script src="https://unpkg.com/htmx.org@1.9.10"></script> <script src="https://unpkg.com/hyperscript.org@0.9.12"></script> {% compress css %} <link href="{% static 'css/main.css' %}" rel="stylesheet"> {% endcompress %} </head> <body hx-boost="true"> {% include 'components/navbar.html' %} <div class="container mx-auto px-4"> {% if messages %} {% for message in messages %} <div class="alert alert-{{ message.tags }}" hx-swap-oob="true" _="on load wait 5s then add .fade-out then wait 0.5s then remove me"> {{ message }} </div> {% endfor %} {% endif %} {% block content %} {% endblock %} </div> {% compress js %} <script src="{% static 'js/main.js' %}"></script> {% endcompress %} </body> </html>
Example of an advanced HTMX-powered infinite scroll with search:
# views.py from django.core.paginator import Paginator from django.template.loader import render_to_string from django.http import HttpResponse class ArticleListView(View): template_name = 'articles/list.html' partial_template_name = 'articles/partials/article_list.html' def get(self, request): search_query = request.GET.get('q', '') page_number = request.GET.get('page', 1) articles = Article.objects.filter( Q(title__icontains=search_query) | Q(content__icontains=search_query) ).select_related('author').prefetch_related('tags') paginator = Paginator(articles, 10) page_obj = paginator.get_page(page_number) context = { 'page_obj': page_obj, 'search_query': search_query } if request.htmx: html = render_to_string( self.partial_template_name, context, request=request ) return HttpResponse(html) return render(request, self.template_name, context)
<!-- templates/articles/list.html --> <div class="space-y-4"> <input type="text" name="search" hx-get="{% url 'article-list' %}" hx-trigger="keyup changed delay:500ms" hx-target="#article-list" hx-push-url="true" placeholder="Search articles..." value="{{ search_query }}"> <div id="article-list" hx-trigger="revealed" hx-get="{% url 'article-list' %}?page={{ page_obj.next_page_number }}" hx-target="this" hx-swap="beforeend"> {% include 'articles/partials/article_list.html' %} </div> </div>
Modern Frontend Integration
Advanced Tailwind CSS Configuration
Create a sophisticated Tailwind setup with custom configuration:
// tailwind.config.js const colors = require('tailwindcss/colors') module.exports = { content: [ "./templates/**/*.html", "./apps/*/templates/**/*.html", ], theme: { extend: { colors: { primary: colors.blue[600], secondary: colors.gray[600], success: colors.green[500], danger: colors.red[500], warning: colors.yellow[500], }, fontFamily: { sans: ['Inter var', ...defaultTheme.fontFamily.sans], }, spacing: { '128': '32rem', '144': '36rem', }, borderRadius: { '4xl': '2rem', } }, }, plugins: [ require('@tailwindcss/forms'), require('@tailwindcss/typography'), require('@tailwindcss/aspect-ratio'), ], }
Database Design and Advanced Models
Example of sophisticated Django models with type hints and advanced features:
# models.py from django.db import models from django.contrib.auth.models import User from django.utils.text import slugify from typing import Any, List import uuid class BaseModel(models.Model): id = models.UUIDField( primary_key=True, default=uuid.uuid4, editable=False ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: abstract = True class Tag(BaseModel): name = models.CharField(max_length=50, unique=True) slug = models.SlugField(unique=True) def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name) super().save(*args, **kwargs) def __str__(self) -> str: return self.name class Article(BaseModel): class Status(models.TextChoices): DRAFT = 'DR', 'Draft' PUBLISHED = 'PB', 'Published' ARCHIVED = 'AR', 'Archived' title: str = models.CharField(max_length=200) slug: str = models.SlugField(unique=True) content: str = models.TextField() author: Any = models.ForeignKey( User, on_delete=models.CASCADE, related_name='articles' ) tags: List[Tag] = models.ManyToManyField(Tag, related_name='articles') status = models.CharField( max_length=2, choices=Status.choices, default=Status.DRAFT ) featured_image = models.ImageField( upload_to='articles/%Y/%m/', null=True, blank=True ) read_time = models.PositiveIntegerField(default=0) class Meta: ordering = ['-created_at'] indexes = [ models.Index(fields=['status', 'created_at']), models.Index(fields=['author', 'status']), ] def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.title) if not self.read_time and self.content: self.read_time = self.calculate_read_time() super().save(*args, **kwargs) def calculate_read_time(self) -> int: words_per_minute = 200 word_count = len(self.content.split()) return max(1, round(word_count / words_per_minute)) def get_absolute_url(self) -> str: return reverse('article-detail', kwargs={'slug': self.slug})
Authentication and Advanced User Management
Implementation of a sophisticated custom user model with additional features:
# users/models.py (continued) class CustomUser(AbstractUser): class Role(models.TextChoices): ADMIN = 'AD', _('Admin') EDITOR = 'ED', _('Editor') AUTHOR = 'AU', _('Author') SUBSCRIBER = 'SU', _('Subscriber') email = models.EmailField(_('email address'), unique=True) role = models.CharField( max_length=2, choices=Role.choices, default=Role.SUBSCRIBER ) bio = models.TextField(max_length=500, blank=True) birth_date = models.DateField(null=True, blank=True) avatar = models.ImageField(upload_to='avatars/', null=True, blank=True) website = models.URLField(max_length=200, blank=True) location = models.CharField(max_length=100, blank=True) USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username'] def get_full_name(self) -> str: return f"{self.first_name} {self.last_name}" def get_role_display_name(self) -> str: return self.get_role_display() @property def is_admin(self) -> bool: return self.role == self.Role.ADMIN
Advanced Authentication Views
# accounts/views.py from django.contrib.auth import get_user_model from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic import UpdateView from django.urls import reverse_lazy from .forms import CustomUserChangeForm User = get_user_model() class ProfileUpdateView(LoginRequiredMixin, UpdateView): model = User form_class = CustomUserChangeForm template_name = 'accounts/profile_update.html' success_url = reverse_lazy('profile') def get_object(self, queryset=None): return self.request.user def form_valid(self, form): response = super().form_valid(form) messages.success(self.request, 'Profile updated successfully!') return response
Advanced API Development
Create a robust REST API using Django REST Framework:
# api/serializers.py from rest_framework import serializers from core.models import Article, Tag class TagSerializer(serializers.ModelSerializer): class Meta: model = Tag fields = ['id', 'name', 'slug'] class ArticleSerializer(serializers.ModelSerializer): author = serializers.StringRelatedField() tags = TagSerializer(many=True, read_only=True) status_display = serializers.CharField( source='get_status_display', read_only=True ) class Meta: model = Article fields = [ 'id', 'title', 'slug', 'content', 'author', 'tags', 'status', 'status_display', 'featured_image', 'read_time', 'created_at', 'updated_at' ] read_only_fields = ['slug', 'read_time'] class ArticleCreateSerializer(serializers.ModelSerializer): tags = serializers.ListField( child=serializers.CharField(), write_only=True, required=False ) class Meta: model = Article fields = ['title', 'content', 'tags', 'status', 'featured_image'] def create(self, validated_data): tags_data = validated_data.pop('tags', []) article = Article.objects.create(**validated_data) for tag_name in tags_data: tag, _ = Tag.objects.get_or_create(name=tag_name) article.tags.add(tag) return article
API Views with Advanced Features
# api/views.py from rest_framework import viewsets, filters, status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from django_filters.rest_framework import DjangoFilterBackend from .serializers import ArticleSerializer, ArticleCreateSerializer from core.models import Article class ArticleViewSet(viewsets.ModelViewSet): queryset = Article.objects.all() filter_backends = [ DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter ] filterset_fields = ['status', 'author', 'tags'] search_fields = ['title', 'content'] ordering_fields = ['created_at', 'updated_at', 'read_time'] permission_classes = [IsAuthenticated] def get_serializer_class(self): if self.action == 'create': return ArticleCreateSerializer return ArticleSerializer def get_queryset(self): queryset = super().get_queryset() if self.action == 'list': return queryset.select_related('author')\ .prefetch_related('tags') return queryset @action(detail=True, methods=['post']) def publish(self, request, pk=None): article = self.get_object() article.status = Article.Status.PUBLISHED article.save() return Response( {'status': 'published'}, status=status.HTTP_200_OK )
Comprehensive Testing Strategy
# tests/test_models.py from django.test import TestCase from django.contrib.auth import get_user_model from core.models import Article, Tag from datetime import date User = get_user_model() class ArticleTests(TestCase): @classmethod def setUpTestData(cls): cls.user = User.objects.create_user( username='testuser', email='test@example.com', password='testpass123' ) cls.tag = Tag.objects.create(name='Python') cls.article = Article.objects.create( title='Test Article', content='Test Content' * 100, # Create substantial content author=cls.user, status=Article.Status.DRAFT ) cls.article.tags.add(cls.tag) def test_article_creation(self): self.assertEqual(self.article.title, 'Test Article') self.assertEqual(self.article.author, self.user) self.assertTrue(self.article.slug) def test_read_time_calculation(self): self.assertTrue(self.article.read_time > 0) def test_article_str_representation(self): self.assertEqual(str(self.article), self.article.title) def test_article_absolute_url(self): url = self.article.get_absolute_url() self.assertEqual(url, f'/articles/{self.article.slug}/') # tests/test_views.py from django.test import TestCase, Client from django.urls import reverse from django.contrib.auth import get_user_model import json User = get_user_model() class ArticleAPITests(TestCase): def setUp(self): self.client = Client() self.user = User.objects.create_user( username='testuser', email='test@example.com', password='testpass123' ) self.client.login( email='test@example.com', password='testpass123' ) def test_create_article(self): url = reverse('api:article-list') data = { 'title': 'New Article', 'content': 'Article content', 'tags': ['Python', 'Django'], 'status': 'DR' } response = self.client.post( url, data=json.dumps(data), content_type='application/json' ) self.assertEqual(response.status_code, 201) self.assertEqual(Article.objects.count(), 1)
Deployment Configuration
Create a production-ready deployment setup:
# settings/production.py from .base import * DEBUG = False ALLOWED_HOSTS = [ 'yourdomain.com', 'www.yourdomain.com', ] # Security settings SECURE_SSL_REDIRECT = True SECURE_HSTS_SECONDS = 31536000 SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_PRELOAD = True SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') # Static and media files STATIC_ROOT = BASE_DIR / 'staticfiles' MEDIA_ROOT = BASE_DIR / 'mediafiles' # Cache settings CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.redis.RedisCache', 'LOCATION': os.getenv('REDIS_URL'), } } # Email configuration EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = os.getenv('EMAIL_HOST') EMAIL_PORT = int(os.getenv('EMAIL_PORT', 587)) EMAIL_USE_TLS = True EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER') EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD') # Logging configuration LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'file': { 'level': 'ERROR', 'class': 'logging.FileHandler', 'filename': BASE_DIR / 'logs' / 'django.log', }, }, 'loggers': { 'django': { 'handlers': ['file'], 'level': 'ERROR', 'propagate': True, }, }, }
Gunicorn Configuration
# gunicorn.conf.py import multiprocessing bind = "unix:/run/gunicorn.sock" workers = multiprocessing.cpu_count() * 2 + 1 worker_class = "uvicorn.workers.UvicornWorker" max_requests = 1000 max_requests_jitter = 50 timeout = 30 keep_alive = 2 # Logging accesslog = "-" errorlog = "-" loglevel = "info" # Process naming proc_name = "mywebsite"
References and Further Reading
- Django Official Documentation
- Django REST Framework
- HTMX Documentation
- Tailwind CSS Documentation
- Python 3.12 Release Notes
- Django 5.0 Release Notes
- Django Best Practices
- Full Stack Python
- Test-Driven Development with Python
- Two Scoops of Django
Remember to always check the official documentation for the most up-to-date information and best practices. This guide covers the fundamentals and advanced concepts, but Django and Python are constantly evolving with new features and improvements.