Understanding the Python Web Framework Landscape
Choosing the right web framework is one of the most consequential architectural decisions you'll make when building a Python web application. Falcon, Django, and FastAPI represent three distinct philosophies in the Python ecosystem β minimalism and raw performance, full-featured batteries-included development, and modern high-performance async APIs respectively. This tutorial will walk you through each framework's core concepts, provide complete working examples, and equip you with the knowledge to make an informed choice for your next project.
Falcon: The Minimalist High-Performance Framework
π Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →What Falcon Is
Falcon is a minimalist, bare-metal WSGI framework designed for building fast REST APIs and app backends. It was created with a singular focus: maximizing throughput while minimizing overhead. Falcon strips away abstractions like ORMs, template engines, and admin panels, giving you raw control over every request. It's the framework of choice at companies like PayPal, Netflix, and Rackspace where micro-benchmarks translate directly to reduced infrastructure costs.
Why Falcon Matters
Falcon matters because it proves that Python can compete with Go and Node.js in the high-throughput API space. By optimizing the request-response path aggressively β using Cython extensions, minimizing object allocations, and employing a flat routing model β Falcon routinely achieves tens of thousands of requests per second on modest hardware. For teams building hundreds of microservices that need to scale horizontally, Falcon's frugal resource consumption directly reduces cloud bills. It also excels in environments where you need to integrate with existing custom middleware stacks without a framework imposing its own middleware ordering.
How to Use Falcon: A Complete REST API Example
Let's build a complete task management API with Falcon. We'll cover resource classes, middleware, request validation, and testing β all without external dependencies beyond Falcon itself.
Project Structure
falcon_task_api/
βββ app/
β βββ __init__.py
β βββ resources.py
β βββ middleware.py
β βββ models.py
β βββ hooks.py
βββ requirements.txt
βββ app.py
βββ test_api.py
Step 1: Define the Data Models
# falcon_task_api/app/models.py
"""
Pure Python data models β no ORM, just dataclasses and in-memory storage.
In production, replace with your database layer of choice.
"""
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional
import uuid
@dataclass
class Task:
"""A single task item."""
id: str = field(default_factory=lambda: str(uuid.uuid4()))
title: str = ""
description: Optional[str] = None
completed: bool = False
created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
updated_at: Optional[str] = None
def to_dict(self):
return {
"id": self.id,
"title": self.title,
"description": self.description,
"completed": self.completed,
"created_at": self.created_at,
"updated_at": self.updated_at,
}
# In-memory storage for demonstration
_task_store: dict[str, Task] = {}
def list_tasks() -> list[Task]:
return list(_task_store.values())
def get_task(task_id: str) -> Optional[Task]:
return _task_store.get(task_id)
def create_task(task: Task) -> Task:
_task_store[task.id] = task
return task
def update_task(task_id: str, updates: dict) -> Optional[Task]:
task = _task_store.get(task_id)
if task is None:
return None
for key, value in updates.items():
if hasattr(task, key):
setattr(task, key, value)
task.updated_at = datetime.now(timezone.utc).isoformat()
return task
def delete_task(task_id: str) -> bool:
if task_id in _task_store:
del _task_store[task_id]
return True
return False
def seed_demo_data():
"""Seed the store with a few demo tasks."""
for i in range(1, 4):
task = Task(
title=f"Demo Task {i}",
description=f"This is demo task number {i}",
completed=i % 2 == 0,
)
_task_store[task.id] = task
Step 2: Create Hooks for Validation
# falcon_task_api/app/hooks.py
"""
Falcon hooks run before or after resource method execution.
They're ideal for cross-cutting concerns like validation.
"""
import falcon
from falcon.media.validators import jsonschema
# JSON Schema for task creation/update validation
TASK_SCHEMA = {
"type": "object",
"properties": {
"title": {"type": "string", "minLength": 1, "maxLength": 200},
"description": {"type": "string", "maxLength": 1000},
"completed": {"type": "boolean"},
},
"required": ["title"],
}
def validate_task_create(req, resp, resource, params):
"""Hook: validate incoming JSON body for task creation."""
if req.method == "POST":
# Falcon's built-in JSON validator
validator = jsonschema.validate(TASK_SCHEMA)
try:
validator(req.media)
except falcon.HTTPBadRequest as e:
raise falcon.HTTPBadRequest(
title="Invalid task data",
description=str(e.description),
)
def validate_task_update(req, resp, resource, params):
"""Hook: validate PATCH body (at least one field must be present)."""
if req.method == "PATCH":
if not req.media or not isinstance(req.media, dict):
raise falcon.HTTPBadRequest(
title="Missing update data",
description="Request body must contain at least one field to update.",
)
allowed_fields = {"title", "description", "completed"}
if not any(key in allowed_fields for key in req.media):
raise falcon.HTTPBadRequest(
title="Invalid update fields",
description=f"Allowed fields: {', '.join(allowed_fields)}",
)
Step 3: Build Resource Classes
# falcon_task_api/app/resources.py
"""
Falcon resource classes map HTTP methods to Python methods.
Each resource is a class with on_get, on_post, on_patch, on_delete, etc.
"""
import falcon
from .models import (
Task, list_tasks, get_task, create_task, update_task, delete_task,
)
class TaskCollectionResource:
"""Resource for /tasks endpoint β list and create."""
def on_get(self, req, resp):
"""GET /tasks β return all tasks."""
tasks = list_tasks()
resp.media = [task.to_dict() for task in tasks]
resp.status = falcon.HTTP_200
def on_post(self, req, resp):
"""POST /tasks β create a new task."""
task = Task(
title=req.media.get("title", ""),
description=req.media.get("description"),
completed=req.media.get("completed", False),
)
created = create_task(task)
resp.media = created.to_dict()
resp.status = falcon.HTTP_201
resp.location = f"/tasks/{created.id}"
class TaskResource:
"""Resource for /tasks/{task_id} endpoint β get, update, delete a single task."""
def on_get(self, req, resp, task_id):
"""GET /tasks/{task_id} β return a single task."""
task = get_task(task_id)
if task is None:
raise falcon.HTTPNotFound(
title="Task not found",
description=f"No task with ID '{task_id}' exists.",
)
resp.media = task.to_dict()
resp.status = falcon.HTTP_200
def on_patch(self, req, resp, task_id):
"""PATCH /tasks/{task_id} β partially update a task."""
task = get_task(task_id)
if task is None:
raise falcon.HTTPNotFound(
title="Task not found",
description=f"No task with ID '{task_id}' exists.",
)
updated = update_task(task_id, req.media)
resp.media = updated.to_dict()
resp.status = falcon.HTTP_200
def on_delete(self, req, resp, task_id):
"""DELETE /tasks/{task_id} β remove a task."""
if not delete_task(task_id):
raise falcon.HTTPNotFound(
title="Task not found",
description=f"No task with ID '{task_id}' exists.",
)
resp.status = falcon.HTTP_204
Step 4: Add Custom Middleware
# falcon_task_api/app/middleware.py
"""
Falcon middleware hooks into the request processing pipeline.
Each middleware class implements process_request and/or process_response.
"""
import falcon
import time
import logging
logger = logging.getLogger("falcon_api")
class TimingMiddleware:
"""Adds an X-Process-Time header to every response."""
def process_request(self, req, resp):
"""Called before routing. Store the start time."""
req.context.start_time = time.perf_counter()
def process_response(self, req, resp, resource, req_succeeded):
"""Called after the response is built. Compute elapsed time."""
elapsed = (time.perf_counter() - req.context.start_time) * 1000 # ms
resp.set_header("X-Process-Time", f"{elapsed:.3f}ms")
class CORSMiddleware:
"""Simple CORS middleware for cross-origin requests."""
def process_response(self, req, resp, resource, req_succeeded):
resp.set_header("Access-Control-Allow-Origin", "*")
resp.set_header("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
resp.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
resp.set_header("Access-Control-Max-Age", "86400")
def process_request(self, req, resp):
"""Handle preflight OPTIONS requests."""
if req.method == "OPTIONS":
resp.status = falcon.HTTP_200
resp.set_header("Access-Control-Allow-Origin", "*")
resp.set_header("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
resp.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
# Falcon will skip routing after this
raise falcon.HTTPStatus(falcon.HTTP_200)
Step 5: Assemble the Application
# falcon_task_api/app.py
"""
Main application entry point. Wire up resources, middleware, and hooks.
"""
import falcon
from app.resources import TaskCollectionResource, TaskResource
from app.middleware import TimingMiddleware, CORSMiddleware
from app.hooks import validate_task_create, validate_task_update
from app.models import seed_demo_data
# Seed some demo data so the API isn't empty on startup
seed_demo_data()
# Create the Falcon application instance
app = falcon.App(
cors_enable=True, # Falcon 3.x+ has built-in CORS support, but we'll use our middleware for illustration
middleware=[TimingMiddleware(), CORSMiddleware()],
)
# Register resources with their URI templates
collection_resource = TaskCollectionResource()
item_resource = TaskResource()
app.add_route("/tasks", collection_resource)
app.add_route("/tasks/{task_id}", item_resource)
# Attach hooks to the collection resource for validation
app.add_hook("before", validate_task_create, resource=collection_resource)
app.add_hook("before", validate_task_update, resource=item_resource)
# For running directly
if __name__ == "__main__":
from wsgiref.simple_server import make_server
print("Falcon Task API running on http://127.0.0.1:8000")
print("Endpoints: GET/POST /tasks, GET/PATCH/DELETE /tasks/{task_id}")
with make_server("127.0.0.1", 8000, app) as httpd:
httpd.serve_forever()
Step 6: Write Tests
# falcon_task_api/test_api.py
"""
Testing Falcon apps is straightforward β use falcon.testing helpers.
"""
import falcon.testing
import pytest
from app import app
from app.models import _task_store
@pytest.fixture(autouse=True)
def clear_store():
"""Clear the in-memory store before each test."""
_task_store.clear()
# Re-seed for tests that need data
from app.models import seed_demo_data
seed_demo_data()
def test_list_tasks_returns_200():
result = falcon.testing.simulate_get(app, "/tasks")
assert result.status == falcon.HTTP_200
assert isinstance(result.json, list)
def test_create_task_returns_201():
payload = {"title": "Write Falcon tutorial", "description": "A comprehensive guide"}
result = falcon.testing.simulate_post(app, "/tasks", json=payload)
assert result.status == falcon.HTTP_201
assert result.json["title"] == "Write Falcon tutorial"
assert "id" in result.json
def test_create_task_missing_title_returns_400():
payload = {"description": "No title here"}
result = falcon.testing.simulate_post(app, "/tasks", json=payload)
assert result.status == falcon.HTTP_400
def test_get_single_task_returns_200():
# Get list and pick the first task's ID
list_result = falcon.testing.simulate_get(app, "/tasks")
task_id = list_result.json[0]["id"]
result = falcon.testing.simulate_get(app, f"/tasks/{task_id}")
assert result.status == falcon.HTTP_200
assert result.json["id"] == task_id
def test_get_nonexistent_task_returns_404():
result = falcon.testing.simulate_get(app, "/tasks/fake-id-12345")
assert result.status == falcon.HTTP_404
def test_delete_task_returns_204():
list_result = falcon.testing.simulate_get(app, "/tasks")
task_id = list_result.json[0]["id"]
result = falcon.testing.simulate_delete(app, f"/tasks/{task_id}")
assert result.status == falcon.HTTP_204
def test_patch_task_returns_200():
list_result = falcon.testing.simulate_get(app, "/tasks")
task_id = list_result.json[0]["id"]
payload = {"completed": True}
result = falcon.testing.simulate_patch(app, f"/tasks/{task_id}", json=payload)
assert result.status == falcon.HTTP_200
assert result.json["completed"] is True
def test_timing_middleware_adds_header():
result = falcon.testing.simulate_get(app, "/tasks")
assert "X-Process-Time" in result.headers
Best Practices for Falcon
- Keep resources thin β delegate business logic to separate service or domain modules. Falcon resources should primarily handle HTTP concerns like status codes, headers, and response serialization.
- Use hooks for cross-cutting concerns β authentication, authorization, request validation, and rate limiting are perfect candidates for Falcon's hook system. This keeps resources clean and testable.
- Leverage req.context β Falcon's request context object is the idiomatic way to pass data between middleware, hooks, and resources within a single request lifecycle.
- Prefer streaming for large responses β Falcon's streaming interface lets you send large payloads without buffering the entire response in memory.
- Benchmark early and often β Falcon's raison d'Γͺtre is performance. Profile your application under realistic load to ensure you're not accidentally introducing bottlenecks.
- Use a production-grade WSGI server β Gunicorn with uvicorn workers (for ASGI compatibility) or meinheld workers will dramatically outperform the built-in wsgiref server.
Django: The Full-Stack Batteries-Included Framework
What Django Is
Django is the venerable, full-featured Python web framework built on the "batteries-included" philosophy. It provides an ORM, admin interface, form handling, authentication system, template engine, and a structured project layout β all out of the box. Django follows the MVT (Model-View-Template) pattern and emphasizes convention over configuration, making it possible to go from zero to a production-ready application in hours rather than weeks. It powers Instagram, Pinterest, Mozilla, and countless other major sites.
Why Django Matters
Django matters because it dramatically compresses development time for data-driven web applications. Its ORM eliminates tedious SQL hand-writing while supporting migrations, complex queries, and multiple database backends. The automatic admin interface gives non-technical stakeholders a way to manage data from day one. Django's mature ecosystem β with thousands of third-party packages, a robust security model, and excellent documentation β means you're never the first person to encounter a problem. For teams building content-heavy sites, e-commerce platforms, or internal tools, Django's comprehensive approach often results in a 3-5x reduction in code volume compared to lighter frameworks.
How to Use Django: A Complete Task Management API
We'll build the same task management API using Django with Django REST Framework (DRF). This will showcase Django's ORM, serializers, viewsets, and the browsable API.
Project Initialization
# Terminal commands to set up the project
# pip install django djangorestframework
# django-admin startproject config .
# python manage.py startapp tasks
# Final project structure:
# config/
# settings.py, urls.py, wsgi.py, asgi.py
# tasks/
# models.py, serializers.py, views.py, urls.py, admin.py, tests.py
# manage.py
Step 1: Define the Django Model
# tasks/models.py
"""
Django models define your database schema declaratively.
Migrations are auto-generated from model changes.
"""
from django.db import models
import uuid
class Task(models.Model):
"""
A task item stored in the database.
Django automatically adds an auto-incrementing 'id' primary key,
but we'll use a UUID field as the public identifier for API parity.
"""
public_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text="Public UUID for external API references",
)
title = models.CharField(max_length=200)
description = models.TextField(blank=True, null=True, max_length=1000)
completed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-created_at"]
verbose_name = "Task"
verbose_name_plural = "Tasks"
def __str__(self):
return self.title[:50]
def toggle_completed(self):
"""Business logic method on the model itself."""
self.completed = not self.completed
self.save(update_fields=["completed", "updated_at"])
Step 2: Create Serializers
# tasks/serializers.py
"""
DRF serializers convert model instances to/from JSON and handle validation.
They're analogous to Falcon's req.media handling but far more declarative.
"""
from rest_framework import serializers
from .models import Task
class TaskSerializer(serializers.ModelSerializer):
"""Serializer for full task representation."""
# Expose the public_id as 'id' in the API for consistency
id = serializers.UUIDField(source="public_id", read_only=True)
class Meta:
model = Task
fields = ["id", "title", "description", "completed", "created_at", "updated_at"]
read_only_fields = ["created_at", "updated_at"]
def validate_title(self, value):
"""Field-level validation: title must be meaningful."""
if len(value.strip()) < 3:
raise serializers.ValidationError("Title must be at least 3 characters long.")
return value.strip()
def validate(self, attrs):
"""Object-level validation: business rules across fields."""
# Example: if a task is marked completed, require a description
if attrs.get("completed") and not attrs.get("description"):
raise serializers.ValidationError(
{"description": "Completed tasks must have a description."}
)
return attrs
class TaskUpdateSerializer(serializers.ModelSerializer):
"""Serializer for partial updates β all fields optional."""
id = serializers.UUIDField(source="public_id", read_only=True)
class Meta:
model = Task
fields = ["id", "title", "description", "completed"]
# No fields are required for PATCH
extra_kwargs = {
"title": {"required": False},
}
Step 3: Build ViewSets
# tasks/views.py
"""
DRF ViewSets group related actions (list, create, retrieve, update, destroy)
into a single class, dramatically reducing boilerplate.
"""
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from .models import Task
from .serializers import TaskSerializer, TaskUpdateSerializer
class TaskViewSet(viewsets.ModelViewSet):
"""
A ViewSet that provides CRUD operations for Task resources.
GET /tasks/ β list all tasks
POST /tasks/ β create a new task
GET /tasks/{public_id}/ β retrieve a single task
PATCH /tasks/{public_id}/ β partial update
DELETE /tasks/{public_id}/ β delete a task
"""
# Query the model using the public_id field for lookup
lookup_field = "public_id"
lookup_url_kwarg = "public_id"
def get_queryset(self):
"""Return the base queryset with optional filtering."""
queryset = Task.objects.all()
# Support ?completed=true / ?completed=false filtering
completed_filter = self.request.query_params.get("completed")
if completed_filter is not None:
completed_bool = completed_filter.lower() == "true"
queryset = queryset.filter(completed=completed_bool)
# Support ?search=term for title/description search
search_term = self.request.query_params.get("search")
if search_term:
queryset = queryset.filter(
models.Q(title__icontains=search_term) |
models.Q(description__icontains=search_term)
)
return queryset
def get_serializer_class(self):
"""Use different serializers for different actions."""
if self.action in ("partial_update", "update"):
return TaskUpdateSerializer
return TaskSerializer
def get_object(self):
"""Retrieve a task by its public_id, returning 404 if not found."""
queryset = self.get_queryset()
obj = get_object_or_404(queryset, public_id=self.kwargs.get("public_id"))
return obj
@action(detail=True, methods=["post"])
def toggle(self, request, public_id=None):
"""Custom action: POST /tasks/{public_id}/toggle/ β toggle completion status."""
task = self.get_object()
task.toggle_completed()
serializer = self.get_serializer(task)
return Response(serializer.data, status=status.HTTP_200_OK)
@action(detail=False, methods=["get"])
def stats(self, request):
"""Custom action: GET /tasks/stats/ β return aggregate statistics."""
from django.db.models import Count, Q
total = Task.objects.count()
completed_count = Task.objects.filter(completed=True).count()
pending_count = total - completed_count
return Response({
"total": total,
"completed": completed_count,
"pending": pending_count,
"completion_rate": round(completed_count / total * 100, 1) if total > 0 else 0,
})
Step 4: Configure URLs and Router
# tasks/urls.py
"""
DRF routers automatically generate URL patterns for ViewSets.
No need to manually map each endpoint.
"""
from rest_framework.routers import DefaultRouter
from .views import TaskViewSet
router = DefaultRouter()
router.register(r"tasks", TaskViewSet, basename="task")
urlpatterns = router.urls
# This auto-generates:
# GET /tasks/ -> task-list
# POST /tasks/ -> task-create
# GET /tasks/{public_id}/ -> task-detail
# PATCH /tasks/{public_id}/ -> task-partial-update
# DELETE /tasks/{public_id}/ -> task-destroy
# POST /tasks/{public_id}/toggle/ -> task-toggle
# GET /tasks/stats/ -> task-stats
# config/urls.py
"""
Root URL configuration β include the tasks router URLs.
"""
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("tasks.urls")), # All API routes under /api/
]
Step 5: Configure Django Settings
# config/settings.py (relevant additions)
"""
Add 'rest_framework' and 'tasks' to INSTALLED_APPS.
Configure DRF settings for pagination, authentication, etc.
"""
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# Third party
"rest_framework",
# Local apps
"tasks",
]
# Django REST Framework configuration
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 50,
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticatedOrReadOnly",
],
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"anon": "100/day",
"user": "1000/day",
},
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.BrowsableAPIRenderer",
],
# Exception handling
"EXCEPTION_HANDLER": "rest_framework.views.exception_handler",
}
Step 6: Register the Admin Interface
# tasks/admin.py
"""
Django's automatic admin gives you a GUI for data management.
Just register your model β Django handles the rest.
"""
from django.contrib import admin
from .models import Task
@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
list_display = ["public_id", "title", "completed", "created_at", "updated_at"]
list_filter = ["completed", "created_at"]
search_fields = ["title", "description"]
readonly_fields = ["public_id", "created_at", "updated_at"]
ordering = ["-created_at"]
actions = ["mark_completed", "mark_pending"]
@admin.action(description="Mark selected tasks as completed")
def mark_completed(self, request, queryset):
queryset.update(completed=True)
@admin.action(description="Mark selected tasks as pending")
def mark_pending(self, request, queryset):
queryset.update(completed=False)
Step 7: Write Django Tests
# tasks/tests.py
"""
Django's test framework integrates seamlessly with DRF.
Use APITestCase for full integration tests with the request factory.
"""
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from .models import Task
class TaskAPITestCase(APITestCase):
"""Comprehensive tests for the Task API."""
def setUp(self):
"""Create sample data for each test."""
self.task1 = Task.objects.create(
title="Learn Django",
description="Complete the Django tutorial",
completed=False,
)
self.task2 = Task.objects.create(
title="Write tests",
description="Test all API endpoints",
completed=True,
)
self.list_url = reverse("task-list")
def test_list_tasks(self):
response = self.client.get(self.list_url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["results"]), 2)
def test_create_task(self):
payload = {"title": "New Task", "description": "A brand new task"}
response = self.client.post(self.list_url, payload, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Task.objects.count(), 3)
self.assertEqual(response.data["title"], "New Task")
def test_create_task_missing_title(self):
payload = {"description": "No title"}
response = self.client.post(self.list_url, payload, format="json")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_retrieve_task(self):
detail_url = reverse("task-detail", kwargs={"public_id": str(self.task1.public_id)})
response = self.client.get(detail_url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["title"], "Learn Django")
def test_partial_update_task(self):
detail_url = reverse("task-detail", kwargs={"public_id": str(self.task1.public_id)})
payload = {"completed": True}
response = self.client.patch(detail_url, payload, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.task1.refresh_from_db()
self.assertTrue(self.task1.completed)
def test_delete_task(self):
detail_url = reverse("task-detail", kwargs={"public_id": str(self.task1.public_id)})
response = self.client.delete(detail_url)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(Task.objects.count(), 1)
def test_toggle_action(self):
toggle_url = reverse("task-toggle", kwargs={"public_id": str(self.task1.public_id)})
response = self.client.post(toggle_url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.task1.refresh_from_db()
self.assertTrue(self.task1.completed)
def test_stats_action(self):
stats_url = reverse("task-stats")
response = self.client.get(stats_url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["total"], 2)
self.assertEqual(response.data["completed"], 1)
def test_filter_by_completed(self):
response = self.client.get(self.list_url + "?completed=true")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["results"]), 1)
self.assertEqual(response.data["results"][0]["title"], "Write tests")
Best Practices for Django
- Keep business logic in models or service layers β Django models should encapsulate domain logic. For complex workflows, introduce a dedicated service layer to avoid fat models.
- Use Django's built-in security features β enable CSRF protection, use parameterized queries (the ORM does this automatically), set SECURE_HSTS_SECONDS, and never disable Django's auto-escaping in templates.
- Structure large projects with apps β Django apps are reusable modules. Split functionality into focused apps (e.g., accounts, payments, notifications) rather than one monolithic app.
- Leverage Django's caching framework β for read-heavy endpoints, Django's cache framework with Redis or Memcached backends can dramatically reduce database load.
- Use database migrations religiously β never edit the database schema manually. Let Django's migration system track every change for reproducible deployments.
- Prefer Django's test Client over raw HTTP tests β the Django test client doesn't require a running server, making tests faster and more reliable. Use APITestCase from DRF for API-specific testing.
FastAPI: The Modern Async High-Performance Framework
What FastAPI Is
FastAPI is a modern Python web framework built on top of Starlette (for the web layer) and Pydantic (for data validation). It's designed from the ground up for building APIs with Python 3.7+ type hints. FastAPI automatically generates OpenAPI (Swagger) documentation, provides async/await support as a first-class citizen, and achieves performance on par with Node.js and Go thanks to its asynchronous core. It was created by SebastiΓ‘n RamΓrez and has become the fastest-growing Python framework since its 2018 debut.
Why FastAPI Matters
FastAPI matters because it bridges the gap between developer productivity and raw performance. Its use of Python type hints isn't cosmetic β it drives automatic request validation, serialization, and documentation generation, eliminating entire categories of boilerplate code. The async-first design means you can handle thousands of concurrent connections without the complexity of thread pools or additional infrastructure. FastAPI's automatic OpenAPI generation integrates seamlessly with tools like Swagger UI and ReDoc, and its dependency injection system provides a clean, testable way to compose complex application logic. For teams adopting microservices, real-time applications, or machine learning model serving, FastAPI offers the best balance of speed and developer experience in the Python