Modern Flask Project Setup in 2026

Flask remains one of the most popular Python web frameworks in 2026, known for its simplicity and flexibility. This guide walks you through setting up a modern Flask project using the latest tools and best practices.

What’s New in 2026?

  • uv - Blazing fast Python package manager (replaces pip/poetry)
  • Flask 3.x - Native async support, improved typing
  • SQLAlchemy 2.0 - Modern ORM with better type hints
  • Pydantic v2 - Fast data validation
  • Ruff - All-in-one linter and formatter (replaces black, isort, flake8)
  • Python 3.12+ - Performance improvements, better error messages

Project Structure

flask-modern/
├── app/
│ ├── __init__.py
│ ├── config.py
│ ├── extensions.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── user.py
│ ├── schemas/
│ │ ├── __init__.py
│ │ └── user.py
│ ├── api/
│ │ ├── __init__.py
│ │ └── users.py
│ └── services/
│ ├── __init__.py
│ └── user_service.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ └── test_users.py
├── migrations/
├── pyproject.toml
├── Dockerfile
├── compose.yaml
└── .env.example

Step 1: Install uv

uv is the modern Python package manager - it’s 10-100x faster than pip.

# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

# Or with pip (if you must)
pip install uv

Step 2: Initialize Project

# Create project directory
mkdir flask-modern && cd flask-modern

# Initialize with uv (creates pyproject.toml and .venv)
uv init --python 3.12

# Or specify exact version
uv python install 3.12
uv venv

Step 3: Configure pyproject.toml

Replace the generated pyproject.toml:

[project]
name = "flask-modern"
version = "0.1.0"
description = "A modern Flask application"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"flask>=3.0.0",
"flask-sqlalchemy>=3.1.0",
"flask-migrate>=4.0.0",
"sqlalchemy>=2.0.0",
"pydantic>=2.5.0",
"pydantic-settings>=2.1.0",
"psycopg2-binary>=2.9.9",
"gunicorn>=21.0.0",
"python-dotenv>=1.0.0",
]

[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-cov>=4.1.0",
"ruff>=0.1.0",
"httpx>=0.26.0", # For testing
]

[tool.ruff]
target-version = "py312"
line-length = 100

[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"SIM", # flake8-simplify
]
ignore = ["E501"] # Line too long (handled by formatter)

[tool.ruff.format]
quote-style = "double"
indent-style = "space"

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
addopts = "-v --tb=short"

Step 4: Install Dependencies

# Install all dependencies (including dev)
uv sync --all-extras

# Or just production dependencies
uv sync

Step 5: Create Configuration

Create app/config.py:

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)

# App settings
app_name: str = "Flask Modern"
debug: bool = False
secret_key: str = "change-me-in-production"

# Database
database_url: str = "sqlite:///app.db"

# Optional: separate components
db_host: str = "localhost"
db_port: int = 5432
db_user: str = "postgres"
db_password: str = "password"
db_name: str = "flask_modern"

@property
def postgres_url(self) -> str:
return f"postgresql://{self.db_user}:{self.db_password}@{self.db_host}:{self.db_port}/{self.db_name}"


settings = Settings()

Create .env.example:

APP_NAME="Flask Modern"
DEBUG=true
SECRET_KEY=your-secret-key-here

# Database (choose one)
DATABASE_URL=sqlite:///app.db
# DATABASE_URL=postgresql://postgres:password@localhost:5432/flask_modern

# Or use separate components
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=password
DB_NAME=flask_modern

Step 6: Set Up Extensions

Create app/extensions.py:

from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

db = SQLAlchemy()
migrate = Migrate()

Step 7: Create Models

Create app/models/__init__.py:

from .user import User

__all__ = ["User"]

Create app/models/user.py:

from datetime import datetime, UTC
from sqlalchemy import String, Boolean, DateTime
from sqlalchemy.orm import Mapped, mapped_column
from app.extensions import db


class User(db.Model):
__tablename__ = "users"

id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
name: Mapped[str] = mapped_column(String(100))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(UTC)
)
updated_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), onupdate=lambda: datetime.now(UTC)
)

def __repr__(self) -> str:
return f"<User {self.email}>"

def to_dict(self) -> dict:
return {
"id": self.id,
"email": self.email,
"name": self.name,
"is_active": self.is_active,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}

Step 8: Create Pydantic Schemas

Create app/schemas/__init__.py:

from .user import UserCreate, UserUpdate, UserResponse

__all__ = ["UserCreate", "UserUpdate", "UserResponse"]

Create app/schemas/user.py:

from pydantic import BaseModel, EmailStr, ConfigDict
from datetime import datetime


class UserBase(BaseModel):
email: EmailStr
name: str


class UserCreate(UserBase):
pass


class UserUpdate(BaseModel):
name: str | None = None
is_active: bool | None = None


class UserResponse(UserBase):
model_config = ConfigDict(from_attributes=True)

id: int
is_active: bool
created_at: datetime
updated_at: datetime | None = None

Step 9: Create Services

Create app/services/__init__.py:

from .user_service import UserService

__all__ = ["UserService"]

Create app/services/user_service.py:

from sqlalchemy import select
from app.extensions import db
from app.models import User
from app.schemas import UserCreate, UserUpdate


class UserService:
@staticmethod
def get_all(skip: int = 0, limit: int = 100) -> list[User]:
stmt = select(User).offset(skip).limit(limit)
return list(db.session.scalars(stmt))

@staticmethod
def get_by_id(user_id: int) -> User | None:
return db.session.get(User, user_id)

@staticmethod
def get_by_email(email: str) -> User | None:
stmt = select(User).where(User.email == email)
return db.session.scalar(stmt)

@staticmethod
def create(data: UserCreate) -> User:
user = User(email=data.email, name=data.name)
db.session.add(user)
db.session.commit()
db.session.refresh(user)
return user

@staticmethod
def update(user: User, data: UserUpdate) -> User:
update_data = data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(user, key, value)
db.session.commit()
db.session.refresh(user)
return user

@staticmethod
def delete(user: User) -> None:
db.session.delete(user)
db.session.commit()

Step 10: Create API Routes

Create app/api/__init__.py:

from flask import Blueprint

api_bp = Blueprint("api", __name__, url_prefix="/api")

from . import users # noqa: E402, F401

Create app/api/users.py:

from flask import request, jsonify
from pydantic import ValidationError
from app.api import api_bp
from app.schemas import UserCreate, UserUpdate, UserResponse
from app.services import UserService


@api_bp.route("/users", methods=["GET"])
def get_users():
skip = request.args.get("skip", 0, type=int)
limit = request.args.get("limit", 100, type=int)
users = UserService.get_all(skip=skip, limit=limit)
return jsonify([UserResponse.model_validate(u).model_dump() for u in users])


@api_bp.route("/users/<int:user_id>", methods=["GET"])
def get_user(user_id: int):
user = UserService.get_by_id(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
return jsonify(UserResponse.model_validate(user).model_dump())


@api_bp.route("/users", methods=["POST"])
def create_user():
try:
data = UserCreate.model_validate(request.json)
except ValidationError as e:
return jsonify({"error": e.errors()}), 422

if UserService.get_by_email(data.email):
return jsonify({"error": "Email already registered"}), 400

user = UserService.create(data)
return jsonify(UserResponse.model_validate(user).model_dump()), 201


@api_bp.route("/users/<int:user_id>", methods=["PATCH"])
def update_user(user_id: int):
user = UserService.get_by_id(user_id)
if not user:
return jsonify({"error": "User not found"}), 404

try:
data = UserUpdate.model_validate(request.json)
except ValidationError as e:
return jsonify({"error": e.errors()}), 422

updated = UserService.update(user, data)
return jsonify(UserResponse.model_validate(updated).model_dump())


@api_bp.route("/users/<int:user_id>", methods=["DELETE"])
def delete_user(user_id: int):
user = UserService.get_by_id(user_id)
if not user:
return jsonify({"error": "User not found"}), 404

UserService.delete(user)
return "", 204

Step 11: Create Application Factory

Create app/__init__.py:

from flask import Flask
from app.config import settings
from app.extensions import db, migrate


def create_app(config_override: dict | None = None) -> Flask:
app = Flask(__name__)

# Configuration
app.config["SECRET_KEY"] = settings.secret_key
app.config["SQLALCHEMY_DATABASE_URI"] = settings.database_url
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

# Override config for testing
if config_override:
app.config.update(config_override)

# Initialize extensions
db.init_app(app)
migrate.init_app(app, db)

# Register blueprints
from app.api import api_bp
app.register_blueprint(api_bp)

# Health check endpoint
@app.route("/health")
def health():
return {"status": "healthy"}

return app

Step 12: Create Entry Point

Create run.py in project root:

from app import create_app

app = create_app()

if __name__ == "__main__":
app.run(debug=True)

Step 13: Set Up Tests

Create tests/__init__.py:

# Empty

Create tests/conftest.py:

import pytest
from app import create_app
from app.extensions import db


@pytest.fixture
def app():
app = create_app({
"TESTING": True,
"SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:",
})

with app.app_context():
db.create_all()
yield app
db.drop_all()


@pytest.fixture
def client(app):
return app.test_client()


@pytest.fixture
def runner(app):
return app.test_cli_runner()

Create tests/test_users.py:

import json


def test_health_check(client):
response = client.get("/health")
assert response.status_code == 200
assert response.json["status"] == "healthy"


def test_create_user(client):
response = client.post(
"/api/users",
data=json.dumps({"email": "test@example.com", "name": "Test User"}),
content_type="application/json",
)
assert response.status_code == 201
data = response.json
assert data["email"] == "test@example.com"
assert data["name"] == "Test User"
assert data["is_active"] is True


def test_create_user_duplicate_email(client):
# Create first user
client.post(
"/api/users",
data=json.dumps({"email": "test@example.com", "name": "Test User"}),
content_type="application/json",
)

# Try to create duplicate
response = client.post(
"/api/users",
data=json.dumps({"email": "test@example.com", "name": "Another User"}),
content_type="application/json",
)
assert response.status_code == 400


def test_get_users(client):
# Create users
client.post(
"/api/users",
data=json.dumps({"email": "user1@example.com", "name": "User 1"}),
content_type="application/json",
)
client.post(
"/api/users",
data=json.dumps({"email": "user2@example.com", "name": "User 2"}),
content_type="application/json",
)

response = client.get("/api/users")
assert response.status_code == 200
assert len(response.json) == 2


def test_get_user_not_found(client):
response = client.get("/api/users/999")
assert response.status_code == 404


def test_update_user(client):
# Create user
create_response = client.post(
"/api/users",
data=json.dumps({"email": "test@example.com", "name": "Test User"}),
content_type="application/json",
)
user_id = create_response.json["id"]

# Update user
response = client.patch(
f"/api/users/{user_id}",
data=json.dumps({"name": "Updated Name"}),
content_type="application/json",
)
assert response.status_code == 200
assert response.json["name"] == "Updated Name"


def test_delete_user(client):
# Create user
create_response = client.post(
"/api/users",
data=json.dumps({"email": "test@example.com", "name": "Test User"}),
content_type="application/json",
)
user_id = create_response.json["id"]

# Delete user
response = client.delete(f"/api/users/{user_id}")
assert response.status_code == 204

# Verify deleted
get_response = client.get(f"/api/users/{user_id}")
assert get_response.status_code == 404

Step 14: Docker Setup

Create Dockerfile:

FROM python:3.12-slim

WORKDIR /app

# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv

# Copy project files
COPY pyproject.toml .
COPY app/ ./app/
COPY run.py .

# Install dependencies
RUN uv sync --frozen --no-dev

# Run with gunicorn
CMD ["uv", "run", "gunicorn", "--bind", "0.0.0.0:8000", "run:app"]

Create compose.yaml:

services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: flask_modern
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d flask_modern"]
interval: 5s
timeout: 5s
retries: 5

api:
build: .
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://postgres:password@db:5432/flask_modern
SECRET_KEY: your-production-secret-key
DEBUG: "false"
depends_on:
db:
condition: service_healthy
volumes:
- ./app:/app/app # Development hot-reload
command: uv run flask run --host 0.0.0.0 --port 8000 --debug

volumes:
postgres_data:

Step 15: Running the Application

Local Development

# Copy environment file
cp .env.example .env

# Initialize database
uv run flask db init
uv run flask db migrate -m "Initial migration"
uv run flask db upgrade

# Run development server
uv run flask run --debug

# Or use the run.py
uv run python run.py

With Docker

# Start services
docker compose up -d --build

# Run migrations
docker compose exec api uv run flask db upgrade

# View logs
docker compose logs -f api

# Stop
docker compose down

Run Tests

# Run all tests
uv run pytest

# With coverage
uv run pytest --cov=app --cov-report=html

# Specific test file
uv run pytest tests/test_users.py -v

Lint and Format

# Check for issues
uv run ruff check .

# Auto-fix issues
uv run ruff check --fix .

# Format code
uv run ruff format .

Common Commands Reference

# Package management
uv add flask # Add dependency
uv add --dev pytest # Add dev dependency
uv remove package-name # Remove dependency
uv sync # Install all dependencies
uv lock # Update lock file

# Flask CLI
uv run flask --help # Show commands
uv run flask routes # List all routes
uv run flask shell # Interactive shell

# Database migrations
uv run flask db init # Initialize migrations
uv run flask db migrate -m "msg" # Create migration
uv run flask db upgrade # Apply migrations
uv run flask db downgrade # Rollback migration

# Testing
uv run pytest -v # Verbose output
uv run pytest -x # Stop on first failure
uv run pytest -k "test_create" # Run matching tests

Why This Stack?

Tool Why Use It
uv 10-100x faster than pip, built-in venv management
Flask 3.x Mature, flexible, great ecosystem
SQLAlchemy 2.0 Type-safe ORM, excellent performance
Pydantic v2 Fast validation, great DX
Ruff Single tool replaces black + isort + flake8
pytest Industry standard, great plugins

Next Steps

  • Add authentication (Flask-JWT-Extended or Authlib)
  • Add caching (Flask-Caching with Redis)
  • Add background tasks (Celery or RQ)
  • Add API documentation (Flask-RESTX or Flasgger)
  • Set up CI/CD (GitHub Actions)
  • Add logging and monitoring

Conclusion

This modern Flask setup gives you a solid foundation for building production-ready applications in 2026. The combination of uv for package management, SQLAlchemy 2.0 for database operations, Pydantic for validation, and Ruff for code quality creates a fast, type-safe, and maintainable codebase.

The project structure separates concerns cleanly: models for database schema, schemas for validation, services for business logic, and API routes for HTTP handling. This makes the code easy to test and extend.


   Reprint policy


《Modern Flask Project Setup in 2026》 by Isaac Zhou is licensed under a Creative Commons Attribution 4.0 International License
 Previous
Dockerizing Your FastAPI Application Dockerizing Your FastAPI Application
Learn how to containerize your FastAPI application with Docker, from writing a Dockerfile to using Docker Compose for multi-service deployments.
2026-01-03
Next 
Getting Started with uv - The Modern Python Package Manager Getting Started with uv - The Modern Python Package Manager
Learn how to use uv, the blazingly fast Python package and project manager written in Rust, to install Python, manage dependencies, and streamline your development workflow.
2026-01-01
  TOC