Django vs Flask vs FastAPI: Choosing the Right Python Web Framework
When building modern web applications with Python, three frameworks dominate the conversation: Django, Flask, and FastAPI. Each serves distinct purposes and excels in different scenarios. This comprehensive guide will walk you through what each framework offers, why the choice matters, practical implementation examples, and best practices to help you make an informed decision.
What Are These Frameworks?
đ Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Django: The Full-Stack Batteries-Included Framework
Django is a high-level, opinionated web framework that ships with everything you need to build complex, database-driven applications. It follows the "batteries-included" philosophy, providing an ORM, authentication system, admin interface, form handling, and templating engine out of the box. Django enforces a clean MVC (Model-View-Controller) architectureâreferred to as MTV (Model-Template-View) in Django terminologyâmaking it ideal for large-scale applications where consistency and rapid development are priorities.
Django powers some of the world's largest websites, including Instagram, Mozilla, and Pinterest. Its built-in admin interface alone can save weeks of development time by automatically generating CRUD interfaces for your models.
Flask: The Lightweight Microframework
Flask is a minimalist, unopinionated microframework that gives you the essentialsârouting, request handling, and templatingâwhile leaving architectural decisions entirely up to you. It's built on Werkzeug (WSGI toolkit) and Jinja2 (templating engine). Flask's strength lies in its simplicity and flexibility; you can add exactly the components you need through a rich ecosystem of extensions, such as Flask-SQLAlchemy for database operations, Flask-Login for authentication, and Flask-Admin for administration interfaces.
Flask is perfect for small to medium-sized applications, APIs, microservices, and prototypes where you want granular control without the overhead of a full-stack framework. Companies like Netflix, Lyft, and Reddit use Flask for various internal and external services.
FastAPI: The Modern High-Performance Framework
FastAPI is a relatively new framework designed specifically for building APIs with Python 3.6+ type hints. It's built on top of Starlette (for the web parts) and Pydantic (for data validation). FastAPI automatically generates interactive API documentation via OpenAPI and JSON Schema standards, provides asynchronous support out of the box, and delivers exceptional performance comparable to Node.js and Go frameworks thanks to its async-first design.
FastAPI has been adopted rapidly by organizations like Microsoft, Uber, and Netflix for building high-performance microservices and machine learning model serving endpoints.
Why the Choice Matters
Selecting the right framework significantly impacts your project's development speed, performance characteristics, maintenance burden, and scalability. Here's why you should carefully evaluate your options:
- Development Velocity: Django's built-in components accelerate feature development for standard web applications, while Flask requires manual integration of extensions. FastAPI's automatic validation and documentation generation can dramatically speed up API development.
- Performance Requirements: FastAPI's async capabilities handle high-concurrency workloads efficiently. Django and Flask, being primarily synchronous (WSGI-based), may require additional infrastructure for handling thousands of concurrent connections.
- Learning Curve: Flask is the easiest to learn, Django has the steepest initial learning curve but pays dividends on large projects, and FastAPI sits in the middle with its intuitive type-based approach.
- Ecosystem Maturity: Django and Flask have decades of packages, tutorials, and community support. FastAPI is newer but growing rapidly with strong backing from the Python community.
- Application Type: A content-heavy website with authentication and admin needs aligns perfectly with Django. A simple REST API or microservice fits Flask or FastAPI better. A real-time or high-throughput API demands FastAPI's async capabilities.
Practical Implementation Examples
Building a Simple REST API: Django Version
Django requires more setup but provides a robust foundation. Here's how to create a basic task management API using Django REST Framework (DRF), the de facto standard for building APIs with Django.
# Install Django and Django REST Framework
# pip install django djangorestframework
# Create project and app
# django-admin startproject taskmanager
# cd taskmanager && django-admin startapp api
# api/models.py
from django.db import models
class Task(models.Model):
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
completed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
# api/serializers.py
from rest_framework import serializers
from .models import Task
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = '__all__'
read_only_fields = ('created_at', 'updated_at')
# api/views.py
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Task
from .serializers import TaskSerializer
class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
@action(detail=True, methods=['post'])
def toggle_complete(self, request, pk=None):
task = self.get_object()
task.completed = not task.completed
task.save()
return Response({'status': 'toggled', 'completed': task.completed})
# taskmanager/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from api.views import TaskViewSet
router = DefaultRouter()
router.register(r'tasks', TaskViewSet)
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include(router.urls)),
]
# taskmanager/settings.py - Add to INSTALLED_APPS
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'api',
]
# Run migrations and start server
# python manage.py makemigrations && python manage.py migrate
# python manage.py runserver
With Django REST Framework, you get automatic URL routing, pagination, filtering, and a browsable API interface at /api/tasks/ without writing additional boilerplate. The ModelViewSet handles all CRUD operations, and custom actions like toggle_complete are easily added.
Building the Same API: Flask Version
Flask gives you complete control over the structure. Here's the equivalent task API using Flask with Flask-SQLAlchemy and Flask-RESTful extensions.
# Install Flask and extensions
# pip install flask flask-sqlalchemy flask-restful flask-migrate
# app.py
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_restful import Api, Resource, reqparse
from flask_migrate import Migrate
from datetime import datetime
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///tasks.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
migrate = Migrate(app, db)
api = Api(app)
# models.py (can be in same file for simplicity)
class Task(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text, default='')
completed = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'title': self.title,
'description': self.description,
'completed': self.completed,
'created_at': self.created_at.isoformat(),
'updated_at': self.updated_at.isoformat()
}
# Task Resource with Flask-RESTful
task_parser = reqparse.RequestParser()
task_parser.add_argument('title', type=str, required=True, help='Title is required')
task_parser.add_argument('description', type=str, default='')
task_parser.add_argument('completed', type=bool, default=False)
class TaskListResource(Resource):
def get(self):
tasks = Task.query.all()
return jsonify([task.to_dict() for task in tasks])
def post(self):
args = task_parser.parse_args()
task = Task(
title=args['title'],
description=args['description'],
completed=args['completed']
)
db.session.add(task)
db.session.commit()
return task.to_dict(), 201
class TaskResource(Resource):
def get(self, task_id):
task = Task.query.get_or_404(task_id)
return task.to_dict()
def put(self, task_id):
task = Task.query.get_or_404(task_id)
args = task_parser.parse_args()
task.title = args['title']
task.description = args.get('description', task.description)
task.completed = args.get('completed', task.completed)
db.session.commit()
return task.to_dict()
def delete(self, task_id):
task = Task.query.get_or_404(task_id)
db.session.delete(task)
db.session.commit()
return {'message': 'Task deleted'}, 204
# Register API endpoints
api.add_resource(TaskListResource, '/api/tasks')
api.add_resource(TaskResource, '/api/tasks/')
# Toggle complete endpoint (alternative approach without Flask-RESTful)
@app.route('/api/tasks//toggle', methods=['POST'])
def toggle_complete(task_id):
task = Task.query.get_or_404(task_id)
task.completed = not task.completed
db.session.commit()
return jsonify({'status': 'toggled', 'completed': task.completed})
# Create database tables
# In terminal: flask db init && flask db migrate && flask db upgrade
if __name__ == '__main__':
app.run(debug=True)
Flask requires you to explicitly define each endpoint, parse request data, and handle serialization manually. This gives you precise control but means more code for common patterns. The flask-restful extension reduces some boilerplate, but you're still responsible for database migrations, error handling, and API documentationâareas where Django provides built-in solutions.
Building the Same API: FastAPI Version
FastAPI leverages Python type hints to provide automatic validation, serialization, and interactive documentation. Here's the task API implementation.
# Install FastAPI and dependencies
# pip install fastapi uvicorn sqlalchemy alembic
# main.py
from fastapi import FastAPI, HTTPException, Depends, status
from fastapi.responses import JSONResponse
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime
import os
# Database setup
SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./tasks.db")
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# SQLAlchemy Model
class TaskModel(Base):
__tablename__ = "tasks"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(200), nullable=False)
description = Column(String, default="")
completed = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
Base.metadata.create_all(bind=engine)
# Pydantic Schemas
class TaskBase(BaseModel):
title: str = Field(..., min_length=1, max_length=200, description="Task title")
description: str = Field("", description="Optional task description")
completed: bool = Field(False, description="Completion status")
class TaskCreate(TaskBase):
pass
class TaskUpdate(BaseModel):
title: Optional[str] = Field(None, min_length=1, max_length=200)
description: Optional[str] = None
completed: Optional[bool] = None
class TaskResponse(TaskBase):
id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
# Dependency for database session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# FastAPI app instance
app = FastAPI(
title="Task Manager API",
description="A simple task management API built with FastAPI",
version="1.0.0"
)
@app.get("/api/tasks", response_model=List[TaskResponse])
async def get_tasks(
skip: int = 0,
limit: int = 100,
completed: Optional[bool] = None,
db: Session = Depends(get_db)
):
"""Retrieve all tasks with optional filtering."""
query = db.query(TaskModel)
if completed is not None:
query = query.filter(TaskModel.completed == completed)
tasks = query.offset(skip).limit(limit).all()
return tasks
@app.post("/api/tasks", response_model=TaskResponse, status_code=status.HTTP_201_CREATED)
async def create_task(task: TaskCreate, db: Session = Depends(get_db)):
"""Create a new task."""
db_task = TaskModel(**task.model_dump())
db.add(db_task)
db.commit()
db.refresh(db_task)
return db_task
@app.get("/api/tasks/{task_id}", response_model=TaskResponse)
async def get_task(task_id: int, db: Session = Depends(get_db)):
"""Retrieve a specific task by ID."""
task = db.query(TaskModel).filter(TaskModel.id == task_id).first()
if task is None:
raise HTTPException(status_code=404, detail="Task not found")
return task
@app.put("/api/tasks/{task_id}", response_model=TaskResponse)
async def update_task(task_id: int, task_update: TaskUpdate, db: Session = Depends(get_db)):
"""Update an existing task."""
task = db.query(TaskModel).filter(TaskModel.id == task_id).first()
if task is None:
raise HTTPException(status_code=404, detail="Task not found")
update_data = task_update.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(task, field, value)
db.commit()
db.refresh(task)
return task
@app.delete("/api/tasks/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_task(task_id: int, db: Session = Depends(get_db)):
"""Delete a task."""
task = db.query(TaskModel).filter(TaskModel.id == task_id).first()
if task is None:
raise HTTPException(status_code=404, detail="Task not found")
db.delete(task)
db.commit()
return JSONResponse(status_code=204)
@app.post("/api/tasks/{task_id}/toggle", response_model=TaskResponse)
async def toggle_task_completion(task_id: int, db: Session = Depends(get_db)):
"""Toggle the completion status of a task."""
task = db.query(TaskModel).filter(TaskModel.id == task_id).first()
if task is None:
raise HTTPException(status_code=404, detail="Task not found")
task.completed = not task.completed
db.commit()
db.refresh(task)
return task
# Run with: uvicorn main:app --reload
FastAPI automatically generates JSON Schema validation from Pydantic models, produces OpenAPI documentation at /docs and /redoc, and supports both synchronous and asynchronous endpoints. The type-hint-driven approach catches errors at development time rather than runtime, significantly reducing bugs related to data handling.
Framework Comparison at a Glance
Django: Strengths and Weaknesses
Strengths: Django excels at rapid development of full-featured web applications. Its ORM is arguably the most mature in Python, supporting complex queries, migrations, and multiple database backends. The admin interface provides instant CRUD operations for models. Authentication, sessions, CSRF protection, and middleware come preconfigured. Django's template engine, form system, and class-based views accelerate building dynamic, form-heavy websites.
Weaknesses: Django's opinionated structure can feel restrictive for unconventional architectures. Its synchronous nature limits performance under high concurrency without additional tooling like Django Channels. The learning curve is steeper initially, and the framework's weight can be excessive for simple microservices or small APIs.
Ideal use cases: Content management systems, e-commerce platforms, social networks, admin-heavy dashboards, and any application requiring robust authentication and authorization workflows.
Flask: Strengths and Weaknesses
Strengths: Flask's minimalism gives developers complete architectural freedom. It's exceptionally easy to learnâa working Flask app can be written in under ten lines of code. The extension ecosystem is vast and mature, allowing you to add precisely what you need. Flask is perfect for microservices, where each service may have unique requirements that don't fit a one-size-fits-all framework.
Weaknesses: Flask's flexibility can lead to inconsistent project structures across teams. Without disciplined architectural patterns, Flask applications can become difficult to maintain as they grow. Security features like CSRF protection, session management, and authentication must be configured manually. The lack of built-in async support (without extensions) means Flask lags behind FastAPI for high-concurrency workloads.
Ideal use cases: REST APIs, microservices, prototype development, serverless functions, small to medium web applications, and projects where team preference favors granular control.
FastAPI: Strengths and Weaknesses
Strengths: FastAPI delivers exceptional performance through native async support, making it suitable for I/O-bound workloads with thousands of concurrent connections. The automatic OpenAPI documentation generation is a massive productivity boost for API consumers. Pydantic's validation ensures data integrity at the boundary of your application. FastAPI's dependency injection system is elegant and powerful for managing database sessions, authentication, and configuration.
Weaknesses: FastAPI is relatively new compared to Django and Flask, so the ecosystem of third-party packages is smaller. It's designed primarily for APIs, not traditional server-rendered HTML applications (though you can serve HTML). Async programming introduces complexity in debugging and requires understanding of event loops and coroutines. For simple CRUD applications, the performance benefits may not justify the async overhead.
Ideal use cases: High-performance APIs, real-time data streaming, machine learning model serving, microservices with high throughput requirements, and any application where auto-generated API documentation provides significant value.
Advanced Patterns and Best Practices
Django Best Practices
- Use Django REST Framework for APIs: Even within a traditional Django app, DRF provides serialization, pagination, throttling, and versioning that surpass manual implementations. Use
ModelViewSetandModelSerializerfor rapid development, but know when to drop to lower-level APIViews for custom logic. - Implement Domain-Driven Design with Services: Avoid fat models by extracting business logic into service layers. This keeps models focused on data persistence and makes unit testing cleaner.
- Leverage Django's Testing Framework: Use
django.test.TestCaseandClientfor integration tests. Combine withpytest-djangofor a more modern testing experience with fixtures and parametrization. - Optimize Query Patterns: Use
select_related()for one-to-one and foreign key relationships, andprefetch_related()for many-to-many and reverse foreign keys. Monitor query counts in development with Django Debug Toolbar.
# Django service layer example
# services/task_service.py
from api.models import Task
from django.db import transaction
class TaskService:
@staticmethod
def complete_overdue_tasks():
"""Mark all overdue tasks with a notification."""
from datetime import datetime
overdue_tasks = Task.objects.filter(
due_date__lt=datetime.now(),
completed=False
)
with transaction.atomic():
count = overdue_tasks.update(completed=True)
return count
@staticmethod
def bulk_create_with_notifications(task_data_list, user):
"""Create multiple tasks and notify the assignee."""
tasks = [Task(**data) for data in task_data_list]
with transaction.atomic():
created_tasks = Task.objects.bulk_create(tasks)
# Send notifications outside transaction
return created_tasks
Flask Best Practices
- Adopt the Application Factory Pattern: Instead of a global app instance, use
create_app()to allow multiple application instances and easier testing. This pattern is essential for larger Flask projects. - Use Blueprints for Modularity: Organize routes into blueprints by domain feature. This prevents monolithic route files and enables reusable components across projects.
- Implement Proper Configuration Management: Use classes for different environments and load sensitive values from environment variables. Avoid hardcoding secrets.
- Handle Errors Consistently: Register global error handlers for common HTTP status codes and use custom exception classes for business logic errors.
# Flask application factory pattern with blueprints
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
import os
db = SQLAlchemy()
migrate = Migrate()
def create_app(config_name=None):
app = Flask(__name__)
# Configuration by environment
env = config_name or os.getenv('FLASK_ENV', 'development')
if env == 'production':
app.config.from_object('app.config.ProductionConfig')
elif env == 'testing':
app.config.from_object('app.config.TestingConfig')
else:
app.config.from_object('app.config.DevelopmentConfig')
# Initialize extensions
db.init_app(app)
migrate.init_app(app, db)
# Register blueprints
from app.routes.tasks import tasks_bp
from app.routes.auth import auth_bp
app.register_blueprint(tasks_bp, url_prefix='/api/tasks')
app.register_blueprint(auth_bp, url_prefix='/api/auth')
# Register error handlers
@app.errorhandler(404)
def not_found(error):
return {'error': 'Resource not found'}, 404
@app.errorhandler(500)
def server_error(error):
return {'error': 'Internal server error'}, 500
return app
# app/routes/tasks.py
from flask import Blueprint, jsonify, request
from app.models import Task
from app import db
tasks_bp = Blueprint('tasks', __name__)
@tasks_bp.route('/')
def list_tasks():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
tasks = Task.query.paginate(page=page, per_page=per_page)
return jsonify({
'items': [t.to_dict() for t in tasks.items],
'total': tasks.total,
'pages': tasks.pages,
'current_page': page
})
FastAPI Best Practices
- Structure with Routers: Use
APIRouterto organize endpoints by domain, mirroring Django's URL includes and Flask's blueprints. This prevents a monolithicmain.pyand improves testability. - Implement Repository Pattern for Data Access: Abstract database operations behind repository classes to decouple business logic from ORM specifics. This simplifies unit testing with mock repositories.
- Use Background Tasks and Celery for Long-Running Operations: Avoid blocking async endpoints with CPU-intensive work. FastAPI's
BackgroundTasksworks for simple fire-and-forget tasks; use Celery or ARQ for distributed task queues. - Validate Everything with Pydantic: Define response models for all endpoints to control exactly what data reaches the client. Use Pydantic's
Fieldconstraints for input validation beyond basic type checking.
# FastAPI modular structure with routers and repository pattern
# routers/tasks.py
from fastapi import APIRouter, Depends, HTTPException, status
from typing import List
from schemas.task import TaskCreate, TaskUpdate, TaskResponse
from repositories.task_repository import TaskRepository
from dependencies import get_task_repository
router = APIRouter(
prefix="/api/tasks",
tags=["tasks"],
responses={404: {"description": "Task not found"}}
)
@router.get("/", response_model=List[TaskResponse])
async def list_tasks(
skip: int = 0,
limit: int = 100,
repo: TaskRepository = Depends(get_task_repository)
):
"""Retrieve tasks with pagination."""
return await repo.get_all(skip=skip, limit=limit)
@router.post("/", response_model=TaskResponse, status_code=status.HTTP_201_CREATED)
async def create_task(
task_data: TaskCreate,
repo: TaskRepository = Depends(get_task_repository)
):
"""Create a new task with validation."""
return await repo.create(task_data)
# repositories/task_repository.py
from sqlalchemy.orm import Session
from models.task import TaskModel
from schemas.task import TaskCreate, TaskUpdate
class TaskRepository:
def __init__(self, session: Session):
self.session = session
async def get_all(self, skip: int = 0, limit: int = 100):
query = self.session.query(TaskModel)
return query.offset(skip).limit(limit).all()
async def get_by_id(self, task_id: int):
return self.session.query(TaskModel).filter(TaskModel.id == task_id).first()
async def create(self, task_data: TaskCreate):
task = TaskModel(**task_data.model_dump())
self.session.add(task)
self.session.commit()
self.session.refresh(task)
return task
# dependencies.py
from sqlalchemy.orm import Session
from repositories.task_repository import TaskRepository
from database import SessionLocal
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
def get_task_repository(db: Session = Depends(get_db)):
return TaskRepository(db)
# main.py
from fastapi import FastAPI
from routers import tasks, auth
app = FastAPI(title="Task Manager API")
app.include_router(tasks.router)
app.include_router(auth.router)
Async Performance Deep Dive
One of the most significant differentiators between these frameworks is their approach to asynchronous processing. Let's examine a practical benchmark scenario.
# FastAPI async endpoint - handles concurrent requests efficiently
# main.py (FastAPI)
import httpx
from fastapi import FastAPI
import asyncio
app = FastAPI()
async def fetch_external_data(url: str):
"""Simulate I/O-bound operation like calling external API."""
async with httpx.AsyncClient() as client:
await asyncio.sleep(0.1) # Simulated network delay
response = await client.get(url, timeout=10.0)
return response.json()
@app.get("/aggregate")
async def aggregate_data():
"""Fetch data from multiple external sources concurrently."""
urls = [
"https://api.service1.com/data",
"https://api.service2.com/data",
"https://api.service3.com/data",
]
# All requests run concurrently with asyncio.gather
results = await asyncio.gather(*[fetch_external_data(url) for url in urls])
return {"aggregated": results}
# Flask equivalent - sequential by default, requires threading for concurrency
# app.py (Flask)
from flask import Flask, jsonify
import requests
from concurrent.futures import ThreadPoolExecutor
app = Flask(__name__)
def fetch_external_data(url):
"""Synchronous I/O-bound operation."""
import time
time.sleep(0.1) # Simulated network delay
response = requests.get(url, timeout=10)
return response.json()
@app.route('/aggregate')
def aggregate_data():
"""Fetch data using thread pool for pseudo-concurrency."""
urls = [
"https://api.service1.com/data",
"https://api.service2.com/data",
"https://api.service3.com/data",
]
with ThreadPoolExecutor(max_workers=3) as executor:
results = list(executor.map(fetch_external_data, urls))
return jsonify({"aggregated": results})
FastAPI's native async support allows handling thousands of concurrent connections with a single process, while Flask requires thread pools or additional worker processes to achieve similar throughput. Django, with its WSGI-based architecture, faces similar limitations to Flask but can be augmented with Django Channels for WebSocket and async support.
Database Integration Comparison
Each framework handles database operations differently. Here's a comparison of the same query across all three:
# Django ORM - declarative, relationship-aware, migration-ready
from api.models import Task, User
# Complex query with relationships, filtering, and aggregation
from django.db.models import Count, Q
completed_by_user = Task.objects.filter(
Q(completed=True) & Q(assigned_to__isnull=False)
).select_related('assigned_to').values(
'assigned_to__username'
).annotate(
completed_count=Count('id')
).order_by('-completed_count')
# Flask SQLAlchemy - similar expressiveness, manual session management
from app.models import Task, User
from app import db
completed_by_user = db.session.query(
User.username,
db.func.count(Task.id).label('completed_count')
).join(Task, Task.assigned_to_id == User.id).filter(
Task.completed == True
).group_by(User.username).order_by(
db.desc('completed_count')
).all()
# FastAPI SQLAlchemy - async-compatible patterns
from sqlalchemy import select, func
from sqlalchemy.orm import joinedload
async def get_completed_by_user(db: Session):
stmt = (
select(User.username, func.count(TaskModel.id).label('completed_count'))
.join(TaskModel, TaskModel.assigned_to_id == User.id)
.where(TaskModel.completed == True)
.group_by(User.username)
.order_by(func.count(TaskModel.id).desc())
)
result = await db.execute(stmt)
return result.all()
Deployment Considerations
Django Deployment
Django applications typically deploy behind a WSGI server like Gunicorn with Nginx as a reverse proxy. For production, you'll configure static file serving via collectstatic, use a production-grade database like PostgreSQL, and implement caching with Redis or Memcached. Django's deployment checklist (python manage.py check --deploy) helps identify security configurations.
# Typical Django production deployment command
# gunicorn taskmanager.wsgi:application \
# --bind 0.0.0.0:8000 \
# --workers 4 \
# --threads 2 \
# --timeout 120 \
# --access-logfile /var/log/gunicorn/access.log \
# --error-logfile /var/log/gunicorn/error.log
Flask Deployment
Flask deploys similarly to Django via Gunicorn, but you must manually handle configuration for different environments. The application factory pattern is essential here to avoid circular imports in production setups.
# Flask production deployment with Gunicorn
# wsgi.py
from app import create_app
application = create_app('production')
# Run with:
# gunicorn wsgi:application --bind 0.0.0.0:8000 --workers 4
FastAPI Deployment
FastAPI uses ASGI servers like Uvicorn or Hypercorn. For production, combine Uvicorn with Gunicorn as a process manager, or use a dedicated ASGI-capable server. FastAPI's async nature pairs well with containerized deployments in Kubernetes for horizontal scaling.
# FastAPI production deployment
# Using Gunicorn with Uvicorn workers
# gunicorn main:app \
# --worker-class uvicorn.workers.UvicornWorker \
# --bind 0.0.0.0:8000 \
# --workers 4 \
# --timeout 120
# Dockerfile example for FastAPI
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Security Features
Security is a critical consideration when choosing a framework. Here's how each handles common security concerns:
- Django: Provides CSRF protection, SQL injection prevention via ORM, XSS protection in templates, clickjacking protection via X-Frame-Options, secure session management, and password hashing with PBKDF2 by default. The admin interface includes permission-based access control.
- Flask: Requires manual configuration for most security features. Flask-Security and Flask-Talisman extensions add CSRF, content security policies, and session management. SQL injection prevention relies on SQLAlchemy or proper parameterized queries.
- FastAPI: Input validation via Pydantic prevents many injection attacks. Dependency injection enables clean authentication patterns. CORS middleware is built-in. Missing