Skip to main content
Python for the JVM Engineer

Web Frameworks Compared

Ravinder··5 min read
PythonJVMJavaFlaskDjangoFastAPISpringweb frameworks
Share:
Web Frameworks Compared

When a Spring Boot engineer evaluates a Python web framework for the first time, the experience is disorienting. Spring is monolithic by default — you get an embedded Tomcat, a DI container, JPA, Spring Security, and a plugin ecosystem without making any choices. Python's web frameworks sit on a spectrum from microframework (Flask) to full-stack batteries-included (Django) to async-first API-focused (FastAPI). None of them is Spring, but together they cover the same problem space.

Framework Positioning

flowchart LR subgraph "Batteries Included" Django["Django\nORM, admin, auth,\nforms, migrations\n(like Spring Boot + JPA)"] end subgraph "Microframework" Flask["Flask\nRouting + WSGI\n(like Spark Java / Javalin)"] end subgraph "Async API" FastAPI["FastAPI\nPydantic + OpenAPI\n(like Spring WebFlux + Springdoc)"] end Flask --> |"add extensions\n(Flask-SQLAlchemy, etc.)"| Django

Pick Flask when you want control. Pick Django when you want decisions made for you. Pick FastAPI when you are building a JSON API and care about performance and automatic documentation.

Flask — Minimal, Explicit

Flask is a WSGI framework (synchronous by default) with no built-in ORM, no authentication, no form handling. It is a router with a request context.

from flask import Flask, jsonify, request, abort
 
app = Flask(__name__)
 
users: dict[int, dict] = {
    1: {"id": 1, "name": "Alice"},
}
 
@app.route("/users/<int:user_id>", methods=["GET"])
def get_user(user_id: int):
    user = users.get(user_id)
    if not user:
        abort(404)
    return jsonify(user)
 
@app.route("/users", methods=["POST"])
def create_user():
    data = request.get_json()
    new_id = max(users) + 1
    users[new_id] = {"id": new_id, **data}
    return jsonify(users[new_id]), 201

Java equivalent using Javalin:

app.get("/users/{id}", ctx -> {
    var id = ctx.pathParamAsClass("id", Integer.class).get();
    var user = users.get(id);
    if (user == null) throw new NotFoundResponse();
    ctx.json(user);
});

Flask has no DI container. Shared dependencies (database connections, config) are typically globals or thread-local proxies (flask.g, current_app). For larger apps this becomes unwieldy — which is why teams either migrate to Django or adopt a DI library like injector.

Django — The Full Stack

Django is opinionated: project layout, ORM, migrations, admin interface, authentication, middleware — all built in and integrated. The closest Spring analogue is Spring Boot with Spring Data JPA and Spring Security pre-configured.

# models.py
from django.db import models
 
class User(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField(unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
 
    class Meta:
        ordering = ["-created_at"]
 
# views.py (Django REST Framework)
from rest_framework import viewsets, serializers
from rest_framework.permissions import IsAuthenticated
 
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ["id", "name", "email", "created_at"]
 
class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [IsAuthenticated]

Django's ORM is closer to Spring Data JPA than raw JDBC — it abstracts SQL into a queryable Python API:

# Equivalent of: SELECT * FROM users WHERE name LIKE 'Al%' ORDER BY created_at
users = User.objects.filter(name__startswith="Al").order_by("created_at")
 
# Equivalent of: SELECT u.*, p.* FROM users u JOIN profiles p ON ...
users = User.objects.select_related("profile").prefetch_related("orders")

Django migrations (python manage.py makemigrations / migrate) are the equivalent of Liquibase or Flyway changesets, but generated automatically from model diffs.

FastAPI — Async, Typed, Documented

FastAPI is an ASGI framework (async) built on Starlette and Pydantic. It generates OpenAPI documentation automatically from type hints and validates requests/responses using Pydantic models.

from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel, EmailStr
 
app = FastAPI(title="User API", version="1.0.0")
 
class UserCreate(BaseModel):
    name: str
    email: EmailStr
 
class UserResponse(BaseModel):
    id: int
    name: str
    email: str
 
users: dict[int, UserResponse] = {}
 
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int) -> UserResponse:
    if user_id not in users:
        raise HTTPException(status_code=404, detail="User not found")
    return users[user_id]
 
@app.post("/users", response_model=UserResponse, status_code=201)
async def create_user(payload: UserCreate) -> UserResponse:
    new_id = len(users) + 1
    user = UserResponse(id=new_id, **payload.model_dump())
    users[new_id] = user
    return user

FastAPI's dependency injection uses Depends() — similar in concept to Spring's @Autowired but explicit and composable:

from fastapi import Depends
from typing import Annotated
 
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
 
DbDep = Annotated[Session, Depends(get_db)]
 
@app.get("/users/{user_id}")
async def get_user(user_id: int, db: DbDep) -> UserResponse:
    ...

Deployment Model

flowchart LR subgraph "Flask/Django (WSGI)" W["gunicorn\n(multi-process)"] --> App1["worker 1"] W --> App2["worker 2"] W --> App3["worker 3"] end subgraph "FastAPI (ASGI)" U["uvicorn\n(async event loop)"] --> FA["single process\n(many coroutines)"] end

Flask and Django deploy behind a WSGI server (Gunicorn, uWSGI) — multiple processes handle concurrent requests. FastAPI deploys behind an ASGI server (Uvicorn, Hypercorn) — a single process handles thousands of concurrent connections via the asyncio event loop, similar to Netty's threading model.

Comparison Summary

Concern Flask Django FastAPI Spring Boot
Routing @app.route urls.py @app.get/post @GetMapping
DI Manual / globals Manual Depends() @Autowired
ORM SQLAlchemy (add) Built-in SQLAlchemy / SQLModel Spring Data JPA
Validation Manual Forms + DRF serializers Pydantic (built-in) Bean Validation
API docs Flask-Smorest drf-spectacular Built-in (Swagger UI) springdoc-openapi
Auth Flask-Login/JWT django-allauth FastAPI-Users Spring Security
Migrations Alembic Built-in Alembic Liquibase/Flyway

Key Takeaways

  • Flask is Javalin/Spark Java — a thin router; you compose the rest from libraries.
  • Django is Spring Boot with JPA and Security included — pick it when you want decisions made for you.
  • FastAPI is Spring WebFlux + Springdoc + Bean Validation in one package — ideal for typed, documented async APIs.
  • All three deploy behind a process manager (Gunicorn for WSGI, Uvicorn for ASGI) rather than an embedded servlet container.
  • FastAPI's Depends() DI is explicit and function-scoped; it lacks Spring's full application-context lifecycle but covers 90% of real use cases cleanly.
  • For new microservices where you control the design, FastAPI is the default choice in 2025 — the type safety, auto-docs, and async model give the most value per line of code.
Share: