← Back to DevBytes

Falcon vs Django vs FastAPI: Framework Comparison

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

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

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

πŸš€ Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started β€” $23.99/mo
← Back to all articles