# Set up a Flask project with uv


[Flask](https://flask.palletsprojects.com/) can start as one file, but one-file apps blur the line between application code, development tools, and production packages. This tutorial uses [uv](https://pydevtools.com/handbook/reference/uv.md) to create a packaged Flask project with locked dependencies and a production-only sync.

## Prerequisites

[Install uv on your system.](https://pydevtools.com/handbook/how-to/how-to-install-uv.md) No separate Python install is required; uv downloads the pinned {{< term "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:

```console
$ 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:

{{< /filetree/folder >}}
    {{< /filetree/folder >}}
  {{< /filetree/folder >}}
{{< /filetree/container >}}

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](https://pydevtools.com/handbook/reference/pyproject.toml.md). The starter file includes these relevant entries:

```toml
[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 {{< term "dependency" "dependencies" >}}:

```console
$ 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](https://docs.pylonsproject.org/projects/waitress/en/latest/) 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 {{< term "lockfile" >}} 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.

```bash
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:

```python {filename="src/flask_board/__init__.py"}
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 app
```

`create_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`:

```python {filename="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`:

```html {filename="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:

```bash
uv run flask --app flask_board run --debug --port 8000
```

The server starts with debug mode and auto-reload enabled:

```console
$ 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](https://pydevtools.com/handbook/reference/pytest.md) and [Ruff](https://pydevtools.com/handbook/reference/ruff.md) as development tools:

```console
$ 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 {{< term "dev-dependency" "development dependencies" >}}, keeping them out of production installs.

Open `pyproject.toml` again. Runtime dependencies stay under `[project]`, while development tools land in `[dependency-groups]`:

```toml
[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:

```python {filename="tests/test_pages.py"}
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.data
```

The test checks for HTTP 200 and the template text.

Run the test:

```console
$ uv run pytest -q
.                                                                        [100%]
1 passed in 0.05s
```

## Lint the project

Run Ruff through uv:

```console
$ 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](https://pydevtools.com/handbook/tutorial/set-up-ruff-for-formatting-and-checking-your-code.md) 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:

```console
$ 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:

```console
$ 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](https://pydevtools.com/handbook/explanation/understanding-dependency-groups-in-uv.md).

## Run with a production server

Run the same app factory with Waitress:

```bash
uv run --no-dev waitress-serve --host 127.0.0.1 --port 8000 --call flask_board:create_app
```

Waitress listens on the same local port:

```console
$ 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:

```console
$ 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:

{{< /filetree/folder >}}
        {{< /filetree/folder >}}
      {{< /filetree/folder >}}
    {{< /filetree/folder >}}
    {{< /filetree/folder >}}
  {{< /filetree/folder >}}
{{< /filetree/container >}}

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](https://pydevtools.com/handbook/tutorial/build-a-production-docker-image-for-a-uv-project.md) turns a uv-managed web app into a deployable container.
- [Setting up GitHub Actions with uv](https://pydevtools.com/handbook/tutorial/setting-up-github-actions-with-uv.md) runs tests and linting on every push.
- [Set up a Django project with uv](https://pydevtools.com/handbook/tutorial/set-up-a-django-project-with-uv.md) applies the same uv workflow to Django's project/app split.
- [Set up a FastAPI project with uv](https://pydevtools.com/handbook/tutorial/set-up-a-fastapi-project-with-uv.md) applies the same uv workflow to an ASGI API.
