Topic Web and software engineering

Build your first web app with Python and Flask

The short answer

To build a Python Flask web app, install Flask in a virtual environment, create an app factory, add routes and Jinja templates, handle a form, store data with Flask-SQLAlchemy, write tests with pytest, then run it behind a production server such as Gunicorn. This tutorial builds a small notes app through each of those steps.

Written by

Siddharth Kajaria, Founder and CEO

Published

Updated

Reading time

7 min read

A glass lab flask holding index cards sits on a cutting mat beside a stack of cards, a red pencil and a desk lamp.
In this post7 min read
  1. Why Flask for a first web app?
  2. What you’ll build, and what you need
  3. Step 1: create the project and a virtual environment
  4. Step 2: the app factory
  5. Step 3: a model with Flask-SQLAlchemy
  6. Step 4: routes and a form
  7. Step 5: templates
  8. Step 6: run it
  9. Step 7: tests
  10. Step 8: a production server
  11. From tutorial to production: what changes when a real product goes live
  12. When should you pick Flask for a project?
  13. How we’d take a Flask prototype to production

Why Flask for a first web app?

Flask is a good choice for a first web app because it is small, readable and explicit: a route is a Python function, a page is a template, and you add a database, forms or logins one piece at a time. You see every part of the app, which is the fastest way to understand how web apps work.

Flask calls itself a microframework. That doesn’t mean it is only for small apps; it means the core stays small and you choose the extensions you need. If you are new to the language itself, start with our guide to Python for beginners, then come back here.

What you’ll build, and what you need

You’ll build a small notes app: a page that lists notes, a form to add one, a database to keep them, tests that prove it works, and a production server to run it. Along the way you’ll use the patterns real Flask projects use, not shortcuts you’d have to unlearn.

You need:

  • Python 3.10 or newer. Check with python3 --version.
  • A terminal and a code editor, such as VS Code or PyCharm.
  • About an hour.

Every command and file below was run end to end with Python 3.11, Flask 3.1, Flask-SQLAlchemy 3.1, SQLAlchemy 2.0, pytest and Gunicorn while writing this tutorial.

Step 1: create the project and a virtual environment

A virtual environment keeps this project’s packages separate from everything else on your computer.

mkdir notes && cd notes
python3 -m venv .venv
source .venv/bin/activate        # on Windows: .venv\Scripts\activate
pip install flask flask-sqlalchemy pytest gunicorn
pip freeze > requirements.txt

Create this layout. The app folder is a Python package, so Flask can find your code and templates:

notes/
  app/
    __init__.py
    models.py
    routes.py
    templates/
      base.html
      index.html
  tests/
    test_app.py

Step 2: the app factory

Instead of creating the app at import time, real Flask projects use an app factory: a function that builds and configures the app. It lets tests create a fresh app with a test database, and production create one with real settings.

app/__init__.py:

import os

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase


class Base(DeclarativeBase):
    pass


db = SQLAlchemy(model_class=Base)


def create_app(test_config=None):
    app = Flask(__name__)
    app.config.from_mapping(
        SECRET_KEY=os.environ.get("SECRET_KEY", "dev"),
        SQLALCHEMY_DATABASE_URI=os.environ.get("DATABASE_URL", "sqlite:///notes.db"),
    )
    if test_config:
        app.config.update(test_config)

    db.init_app(app)

    from . import routes
    app.register_blueprint(routes.bp)

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

    return app

Settings come from environment variables, with safe defaults for your laptop. The secret key signs session cookies, so in production it must be a long random value, never “dev”. The SQLite file is created in an instance folder next to the package the first time the app runs.

Step 3: a model with Flask-SQLAlchemy

A model is a Python class that maps to a database table. Each note has an id, its text and the time it was created.

app/models.py:

from datetime import datetime, timezone

from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column

from . import db


class Note(db.Model):
    id: Mapped[int] = mapped_column(primary_key=True)
    text: Mapped[str] = mapped_column(String(500))
    created: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc))

This is the SQLAlchemy 2.0 style, with type hints describing each column. Older tutorials use db.Column(...); it still works, but new code should use this form.

Step 4: routes and a form

Routes connect URLs to Python functions. A blueprint groups related routes, which keeps the app tidy as it grows. Here the same URL shows the notes on a GET request and saves a new one on a POST.

app/routes.py:

from flask import Blueprint, flash, redirect, render_template, request, url_for

from . import db
from .models import Note

bp = Blueprint("notes", __name__)


@bp.get("/")
def index():
    notes = db.session.scalars(db.select(Note).order_by(Note.created.desc())).all()
    return render_template("index.html", notes=notes)


@bp.post("/")
def add():
    text = request.form.get("text", "").strip()
    if not text:
        flash("Write something first.")
    else:
        db.session.add(Note(text=text))
        db.session.commit()
    return redirect(url_for("notes.index"))

Two habits worth noticing. The form input is cleaned and checked before it is saved, and after a successful POST the app redirects instead of rendering a page, so refreshing the browser doesn’t submit the note twice.

Step 5: templates

Flask renders pages with Jinja templates. A base template holds the shared page structure, and each page fills in its own block. Jinja escapes values by default, so a note containing HTML is shown as text, not run as code.

app/templates/base.html:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Notes</title>
</head>
<body>
  {% for message in get_flashed_messages() %}
    <p>{{ message }}</p>
  {% endfor %}
  {% block content %}{% endblock %}
</body>
</html>

app/templates/index.html:

{% extends "base.html" %}
{% block content %}
  <h1>Notes</h1>
  <form method="post">
    <input name="text" maxlength="500" required>
    <button>Add</button>
  </form>
  <ul>
    {% for note in notes %}
      <li>{{ note.text }}</li>
    {% endfor %}
  </ul>
{% endblock %}

Step 6: run it

From the notes folder, with the virtual environment active:

flask --app app run --debug

Open http://127.0.0.1:5000 in your browser, add a note and refresh. Debug mode reloads the app when you save a file and shows detailed errors in the browser. Never use debug mode on a server anyone else can reach.

Step 7: tests

Tests let you change the app without fear. Flask’s test client sends requests to the app without a browser, and the app factory gives each test a fresh in-memory database.

tests/test_app.py:

import pytest

from app import create_app


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


def test_home_page_loads(client):
    response = client.get("/")
    assert response.status_code == 200
    assert b"<h1>Notes</h1>" in response.data


def test_add_note(client):
    response = client.post("/", data={"text": "Buy milk"}, follow_redirects=True)
    assert b"Buy milk" in response.data


def test_empty_note_is_rejected(client):
    response = client.post("/", data={"text": "   "}, follow_redirects=True)
    assert b"Write something first." in response.data

Run them with python -m pytest -q. You should see three passing tests.

Step 8: a production server

Flask’s built-in server is for development only. In production, a WSGI server such as Gunicorn runs several worker processes and handles real traffic:

export SECRET_KEY="$(python -c 'import secrets; print(secrets.token_hex(32))')"
gunicorn -w 2 -b 0.0.0.0:8000 "app:create_app()"

On Windows, Waitress plays the same role as Gunicorn. In a real deployment, a reverse proxy such as nginx sits in front for HTTPS, or you deploy to a platform that provides it, such as Render, Railway, Fly.io, Google Cloud Run or AWS App Runner.

From tutorial to production: what changes when a real product goes live

A tutorial app and a production app differ less in the code you’ve written than in what surrounds it. Before real users arrive, add:

  • User accounts and permissions. Flask-Login handles sessions; hash passwords with a proper library and never store them in plain text.
  • CSRF protection on every form. Flask-WTF adds tokens so other sites can’t submit forms on a user’s behalf. This tutorial leaves it out for clarity; a live app shouldn’t.
  • Database migrations. Replace db.create_all() with Flask-Migrate (built on Alembic), so schema changes are versioned and reversible.
  • A production database. PostgreSQL or MySQL instead of SQLite once several users or workers write at the same time.
  • Secrets and configuration in environment variables or a secret manager, separate for each environment.
  • Logging and error monitoring, for example Sentry or your host’s logs, so you hear about errors before users report them.
  • Backups you have restored at least once. An untested backup is a hope, not a backup.
  • Pinned dependencies and updates. Keep requirements.txt pinned, and update on a schedule.
  • Automated tests and deploys through a pipeline, so every change is tested the same way.

Our guide on how to build a web application with Python looks at how Flask fits alongside the other Python frameworks as a project grows.

When should you pick Flask for a project?

Pick Flask when the project is an API, a small or medium web app, an internal tool, a prototype, or a service around a Python library or model. Decide by the project’s traits:

Project trait Flask fits A batteries-included framework (such as Django) may fit better
Size of the data model A handful of tables Dozens of related models
Admin interface Not needed, or built to order Needed on day one
Shape of the product An API, a microservice or a focused app A content-heavy site with accounts, roles and admin
Team preference Choose each library yourself Use one set of conventions everywhere
Python libraries at the core A model, a data pipeline or a scientific library to expose Mostly standard web features

How we’d take a Flask prototype to production

When a prototype becomes a product, this is how we’d approach it as a method: review the code against the production list above, move configuration and secrets out of the code, add migrations and a production database, put tests and deploys in a pipeline, add monitoring and backups, then load-test the busiest pages before launch. That work sits within Python web development and custom web app development projects, and if you’d rather add people to your own team, you can hire Python developers.

Planning something like this?

Tell us what you are building, and we will talk it through with you.

Book a call

02 FAQ

Questions, answered

Is Flask good for beginners?
Yes. Flask is small enough to understand completely: a working app fits in one file, and you add pieces such as a database or logins only when you need them. That makes it a good way to learn how web apps work, because nothing happens that you didn't write or install yourself.
What kind of project suits Flask?
Pick Flask for APIs, small to medium web apps, internal tools, prototypes and services that wrap a Python library or a machine learning model. If you need an admin panel, user accounts and a large data model on day one, a batteries-included framework such as Django may save time.
Can Flask run in production?
Yes. Flask apps run in production behind a WSGI server such as Gunicorn, or Waitress on Windows, usually with a reverse proxy such as nginx in front for HTTPS. The built-in development server is only for local work. Add migrations, secrets in environment variables, logging and backups before going live.
Flask or Django for a first project?
Choose Flask if you want to learn how each part of a web app fits together, or if the project is an API or a small app. Choose Django if the project needs user accounts, an admin interface and many database models from the start. Both are mature, well-documented Python frameworks.