Set up a Flask project with uv
Flask can start as one file, but one-file apps blur the line between application code, development tools, and production packages. This tutorial uses uv to create a packaged Flask project with locked dependencies and a production-only sync.
Prerequisites
Install uv on your system. No separate Python install is required; uv downloads the pinned interpreterThe program that reads and executes Python code. When you run "python3 hello.py", python3 is the interpreter. if it is missing.
Create the uv project
Initialize a packaged project and pin Python 3.13 so the generated pyproject.toml is reproducible:
$ uv init --python 3.13 flask-board
Initialized project `flask-board` at `/path/to/flask-board`
$ cd flask-board
The files this tutorial uses look like this:
-
- .python-version
- README.md
- pyproject.toml
-
-
- __init__.py
-
Notice the src/flask_board/ package. Flask will import the application factory from that package, and uv will install it into .venv/ in editable mode during development.
Open pyproject.toml. The starter file includes these relevant entries:
[project]
name = "flask-board"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []
[project.scripts]
flask-board = "flask_board:main"Delete the [project.scripts] table. The starter command points at main, but this project will use Flask’s create_app factory instead.
Note
If uv init creates a flat main.py instead of a src/ package, update uv with uv self update. Flask works in either layout, but the packaged layout keeps imports closer to a production install.
Install Flask and Waitress
Add Flask and a production WSGI server as runtime dependenciesAn external package your project needs, listed in pyproject.toml so tools can install it automatically. :
$ uv add flask waitress
Using CPython 3.13.13
Creating virtual environment at: .venv
Resolved 9 packages in 201ms
Building flask-board @ file:///path/to/flask-board
Built flask-board @ file:///path/to/flask-board
Prepared 2 packages in 70ms
Installed 9 packages in 5ms
+ flask==3.1.3
+ flask-board==0.1.0 (from file:///path/to/flask-board)
+ waitress==3.0.2
...
Flask provides the development server and CLI. Waitress provides a production WSGI server that works on macOS, Linux, and Windows, which keeps the tutorial commands portable.
uv add records both packages in pyproject.toml and writes a lockfileA file that records the exact version of every installed package, so everyone working on the project gets identical installs.
named uv.lock. Commit both files so teammates and deployment systems resolve the same package set.
Replace the starter package with a Flask factory
Create the template directory that the app will render:
uv run executes the command inside the project’s virtual environment, ensuring all dependencies are installed first.
uv run python -c "from pathlib import Path; Path('src/flask_board/templates/pages').mkdir(parents=True, exist_ok=True)"This command prints nothing when it succeeds.
Replace src/flask_board/__init__.py with an application factory:
from flask import Flask
from .pages import bp as pages_bp
def create_app():
app = Flask(__name__)
app.config.from_mapping(SECRET_KEY="dev")
app.register_blueprint(pages_bp)
return appcreate_app returns a configured Flask application. Keeping the app creation inside a function lets tests create a fresh app, and it gives deployment servers one import target: flask_board:create_app.
The SECRET_KEY="dev" value is fine while following the tutorial. Replace it with an environment-specific secret before serving real users.
Register a blueprint
Create src/flask_board/pages.py:
from flask import Blueprint, render_template
bp = Blueprint("pages", __name__)
@bp.get("/")
def home():
return render_template(
"pages/home.html",
title="Flask + uv",
)The blueprint owns the route. The factory imports it and calls app.register_blueprint(pages_bp), so more route groups can be added later without crowding __init__.py.
Create src/flask_board/templates/pages/home.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ title }}</title>
</head>
<body>
<main>
<h1>{{ title }}</h1>
<p>Flask is running inside a uv-managed project.</p>
</main>
</body>
</html>The template path starts at the package’s templates/ directory. render_template("pages/home.html") finds the file at src/flask_board/templates/pages/home.html.
Run the development server
Start Flask through uv:
uv run flask --app flask_board run --debug --port 8000The server starts with debug mode and auto-reload enabled:
$ uv run flask --app flask_board run --debug --port 8000
* Serving Flask app 'flask_board'
* Debug mode: on
* Running on http://127.0.0.1:8000
* Restarting with stat
* Debugger is active!
* Debugger PIN: nnn-nnn-nnn
Visit http://127.0.0.1:8000/. The page should show Flask + uv and the sentence from the template. Press CTRL+C to stop the server.
If port 8000 is already in use, rerun the same command with another port, such as --port 8001. The startup banner will show the new URL.
Test the route
Add pytest and Ruff as development tools:
$ uv add --dev pytest ruff
Resolved 16 packages in 234ms
Building flask-board @ file:///path/to/flask-board
Built flask-board @ file:///path/to/flask-board
Prepared 1 package in 2ms
Uninstalled 1 package in 0.48ms
Installed 7 packages in 12ms
~ flask-board==0.1.0 (from file:///path/to/flask-board)
+ pytest==9.1.1
+ ruff==0.16.5
...
The --dev flag marks pytest and Ruff as development dependenciesA package needed during development (testing, linting, formatting) that is not shipped to users of your project. Installed with the --dev flag.
, keeping them out of production installs.
Open pyproject.toml again. Runtime dependencies stay under [project], while development tools land in [dependency-groups]:
[project]
dependencies = [
"flask>=3.1.3",
"waitress>=3.0.2",
]
[dependency-groups]
dev = [
"pytest>=9.1.1",
"ruff>=0.16.5",
]Create tests/test_pages.py as a smoke test for the route:
from flask_board import create_app
def test_home_page():
app = create_app()
app.config.update(TESTING=True)
client = app.test_client()
response = client.get("/")
assert response.status_code == 200
assert b"Flask is running inside a uv-managed project." in response.dataThe test checks for HTTP 200 and the template text.
Run the test:
$ uv run pytest -q
. [100%]
1 passed in 0.05s
Lint the project
Run Ruff through uv:
$ uv run ruff check .
All checks passed!
Ruff checks the package and test file inside the same environment as Flask and pytest. See Set up Ruff for formatting and checking your code when you are ready to add formatting and stricter rule sets.
Sync production dependencies
uv sync installs the default development group. For production installs, exclude it:
$ uv sync --no-dev
Resolved 16 packages in 1ms
Uninstalled 6 packages in 28ms
- pytest==9.1.1
- ruff==0.16.5
...
The application dependencies remain installed, but pytest and Ruff are removed from .venv/. The dev/prod split lives in pyproject.toml and uv.lock; uv sync --no-dev selects the production install.
Confirm that pytest is no longer importable:
$ uv run --no-dev python -c "import importlib.util; print(importlib.util.find_spec('pytest'))"
None
For dependency groups, defaults, and extras, read Understanding dependency groups in uv.
Run with a production server
Run the same app factory with Waitress:
uv run --no-dev waitress-serve --host 127.0.0.1 --port 8000 --call flask_board:create_appWaitress listens on the same local port:
$ uv run --no-dev waitress-serve --host 127.0.0.1 --port 8000 --call flask_board:create_app
INFO:waitress:Serving on http://127.0.0.1:8000
Use flask run --debug while developing. Use a WSGI server such as Waitress or Gunicorn for production. The Flask development server is for local work only.
Press CTRL+C to stop Waitress, then restore the development environment:
$ uv sync
Resolved 16 packages in 2ms
Installed 6 packages in 6ms
+ pytest==9.1.1
+ ruff==0.16.5
...
Review the project structure
The project now has a Flask package, one blueprint, one template, one test, and a lockfile:
-
- .python-version
- README.md
- pyproject.toml
- uv.lock
-
-
- __init__.py
- pages.py
-
-
- home.html
-
-
-
- test_pages.py
Commit pyproject.toml, uv.lock, src/, tests/, .python-version, and README.md. Do not commit .venv/; uv recreates it with uv sync.
Keep building the project
- Build a production Docker image for a uv project turns a uv-managed web app into a deployable container.
- Setting up GitHub Actions with uv runs tests and linting on every push.
- Set up a Django project with uv applies the same uv workflow to Django’s project/app split.
- Set up a FastAPI project with uv applies the same uv workflow to an ASGI API.