Skip to main content

🗄️ Flask-SQLAlchemy Extension

Flask ships without a database on purpose — it lets you pick the right tool. The tool most Flask developers reach for is Flask-SQLAlchemy, an extension that puts a full-featured ORM one import away. This lesson gets it installed, configured, and creating tables, using the modern SQLAlchemy 2.0 style.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what an ORM is and weigh its benefits against its trade-offs
  • Install and configure Flask-SQLAlchemy with a database URI for SQLite, PostgreSQL, or MySQL
  • Wire the extension into an application factory using db.init_app(app)
  • Create your database tables with db.create_all() inside an application context
  • Identify the roles of the SQLAlchemy engine, session, and metadata

Estimated Time: 30–40 minutes  •  Difficulty: Intermediate

Hands-on: Build a tiny task-tracker backend wired to a real SQLite database.

In This Lesson

Why Flask Has No Built-in Database

Almost every real web application needs to remember things: user accounts, blog posts, orders, tasks. That memory lives in a database. Yet Flask, being a microframework, ships with no database layer at all. That is a deliberate design choice, not an omission — Flask hands you the routing and request handling and lets you choose how to store data.

Flask-SQLAlchemy is the official extension that fills that gap. It wraps SQLAlchemy — the most popular database toolkit in the Python world — and adapts it to Flask's application and request lifecycle so it "just works" with sensible defaults.

💡 A useful analogy: Think of Flask-SQLAlchemy as a translator that sits between two people who don't speak the same language. You speak Python (objects, classes, attributes); the database speaks SQL (tables, rows, columns). The extension translates each request faithfully in both directions so you rarely have to write SQL by hand.

What Is an ORM?

SQLAlchemy is an ORM — an Object-Relational Mapper. It maps Python classes to database tables and class instances to table rows. Instead of writing INSERT INTO users (...), you create a User() object and hand it to the session; the ORM writes the SQL for you.

How an ORM sits between Python and the database Python objects flow into the ORM layer, which translates them into SQL queries that run against the database, and results flow back as Python objects. Python classes & objects ORM SQLAlchemy Database tables & rows (SQL) objects SQL rows objects
Figure 1 — The ORM is a two-way translator. You work with Python objects; it produces the SQL and hydrates the results back into objects.

Why developers reach for an ORM

  • Work with Python objects — familiar classes and attributes instead of query strings.
  • Database-agnostic — the same model code runs on SQLite in development and PostgreSQL in production.
  • SQL-injection protection — the ORM parameterizes every value it sends, so user input can't be smuggled into a query.
  • Less boilerplate — common create/read/update/delete operations shrink to a line or two.
  • Relationships as attributesuser.posts instead of a hand-written JOIN.

⚠️ The trade-offs are real

An ORM adds a learning curve (its API is one more thing to know) and a thin layer of performance overhead. For a handful of unusual, highly-tuned queries you may still drop down to raw SQL — and SQLAlchemy lets you. For the vast majority of application code, the productivity and safety win easily.

Installing & Configuring

Install Flask-SQLAlchemy alongside the driver for whichever database you plan to use. SQLite needs no driver — it is built into Python — which makes it perfect for learning and local development.

# The extension pulls in SQLAlchemy as a dependency
pip install Flask-SQLAlchemy

# A database driver is only needed for non-SQLite engines:
pip install "psycopg[binary]"   # PostgreSQL (modern psycopg 3)
pip install mysqlclient          # MySQL / MariaDB

Here is the smallest possible setup. Notice the modern pieces: we build the extension around a typed DeclarativeBase, and we no longer need to disable SQLALCHEMY_TRACK_MODIFICATIONS — it already defaults to off in Flask-SQLAlchemy 3.x.

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


# A typed declarative base (SQLAlchemy 2.0 style)
class Base(DeclarativeBase):
    pass


# Create the extension object; bind it to an app below
db = SQLAlchemy(model_class=Base)

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///site.db"

# Attach the extension to this application
db.init_app(app)

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

📖 Key Terms

Extension: a package that plugs extra behavior into Flask through its documented hooks.

db object: your handle to the whole ORM — the session, the base model class, and the query helpers all hang off it.

Database URI: a single connection string that tells SQLAlchemy which engine to use and how to reach it.

The Database URI

The SQLALCHEMY_DATABASE_URI is the one setting you truly must provide. Its shape changes with the engine, but the pattern is always dialect+driver://user:password@host:port/database.

DatabaseURI formatExample
SQLitesqlite:///filename.dbsqlite:///site.db
PostgreSQLpostgresql+psycopg://user:pass@host:port/dbpostgresql+psycopg://me:secret@localhost/myapp
MySQL / MariaDBmysql://user:pass@host:port/dbmysql://me:secret@localhost/myapp

⚠️ Never hard-code production secrets

A real password does not belong in your source code. Read the URI from the environment and fall back to SQLite for local development:

import os

app.config["SQLALCHEMY_DATABASE_URI"] = (
    os.environ.get("DATABASE_URL") or "sqlite:///site.db"
)

This one line lets the same code run on SQLite on your laptop and PostgreSQL on the server, controlled entirely by an environment variable.

The Application Factory Pattern

The tiny example above creates app at module import time. That works for a script, but it causes problems as an app grows: you can't easily build a second app with a test configuration, and modules that import app risk circular imports.

The professional solution is the application factory — a function, conventionally create_app(), that builds and returns a fresh Flask instance. The extension is created once at module level unbound, then attached inside the factory with db.init_app(app).

flowchart TD A["db = SQLAlchemy() — created unbound"] --> B["create_app(config)"] B --> C["app = Flask(__name__)"] C --> D["app.config.from_object(config)"] D --> E["db.init_app(app)"] E --> F["register blueprints"] F --> G["return app"]
# app/extensions.py — one place for shared extension objects
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase


class Base(DeclarativeBase):
    pass


db = SQLAlchemy(model_class=Base)
# app/__init__.py — the application factory
from flask import Flask
from .extensions import db
from .config import Config


def create_app(config_class=Config):
    app = Flask(__name__)
    app.config.from_object(config_class)

    # Bind the extension to THIS application
    db.init_app(app)

    # Import models so their tables are registered, then create them
    with app.app_context():
        from . import models  # noqa: F401
        db.create_all()

    # from .routes import main
    # app.register_blueprint(main)

    return app
# app/config.py — configuration as classes
import os


class Config:
    SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL") or "sqlite:///site.db"
    SECRET_KEY = os.environ.get("SECRET_KEY") or "dev-key-change-in-production"


class TestConfig(Config):
    SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"  # fast, disposable
    TESTING = True

✅ Why the factory wins

Because create_app() takes a config argument, your test suite can spin up an app backed by an in-memory database with a single call: app = create_app(TestConfig). Nothing about your models or routes has to change.

Creating the Tables

Defining a model class doesn't create a table by itself — you have to tell SQLAlchemy to issue the CREATE TABLE statements. The method is db.create_all(), and it must run inside an application context so the extension knows which app (and therefore which database) to talk to.

from app import create_app
from app.extensions import db

app = create_app()

with app.app_context():
    db.create_all()   # issues CREATE TABLE for every registered model
    print("Tables created.")

⚠️ create_all() does not alter existing tables

db.create_all() only creates tables that don't yet exist. If you later add a column to a model, it will not change the table that's already there. Evolving a live schema safely is the job of a migration tool — Flask-Migrate (a wrapper around Alembic), which you'll meet in a later lesson.

One more modern touch: because two apps might register the same model, always import your models before calling create_all(), exactly as the factory above does.

Engine, Session & Metadata

Flask-SQLAlchemy hides most of SQLAlchemy's machinery, but three concepts are worth knowing because error messages and advanced features reference them constantly.

ComponentWhat it doesEveryday analogy
EngineManages the pool of connections to the database and speaks its dialect of SQL.The phone line to the database
SessionA staging area that tracks your object changes until you commit() them as one transaction.A shopping cart
MetadataThe in-memory catalog of every table, column, and constraint your models define.The blueprint set
flowchart LR A[Flask app] --> B[Flask-SQLAlchemy] B --> C[SQLAlchemy core] C --> D[Engine + connection pool] C --> E[Session / unit of work] C --> F[Metadata / schema] D --> G[(Database)]

You interact with the session constantly (db.session.add(...), db.session.commit()), you configure the engine indirectly through the URI, and the metadata is what db.create_all() reads to know which tables to build.

Hands-on Exercise

🏋️ Wire Up a Task Tracker

Objective: Stand up a Flask app with Flask-SQLAlchemy, define one model, and create a real SQLite database file.

Instructions:

  1. Create a folder and install the extension: pip install Flask-SQLAlchemy.
  2. Create the db object around a DeclarativeBase, as shown earlier.
  3. Define a Task model with a title, an optional description, a boolean done flag, and a creation timestamp.
  4. Inside an application context, call db.create_all() and confirm a tasks.db file appears on disk.
💡 Hint

Use timezone-aware timestamps — datetime.utcnow is deprecated in modern Python. Pass a callable (no parentheses) as the default so it runs at insert time, not import time: default=lambda: datetime.now(timezone.utc).

✅ Sample solution
from datetime import datetime, timezone
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import String, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column


class Base(DeclarativeBase):
    pass


db = SQLAlchemy(model_class=Base)


class Task(db.Model):
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(100))
    description: Mapped[str | None] = mapped_column(Text)
    done: Mapped[bool] = mapped_column(default=False)
    created_at: Mapped[datetime] = mapped_column(
        default=lambda: datetime.now(timezone.utc)
    )

    def __repr__(self) -> str:
        return f"Task({self.title!r}, done={self.done})"


def create_app():
    app = Flask(__name__)
    app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///tasks.db"
    db.init_app(app)
    with app.app_context():
        db.create_all()
    return app


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

Run it once and a tasks.db file will be created in the instance/ folder. You now have a database ready for the next lesson, where we'll design richer models.

🎯 Quick Quiz

Question 1: What is the primary job of an ORM like SQLAlchemy?

Question 2: In the application factory pattern, how is the db extension connected to a specific app?

Question 3: Why must db.create_all() run inside with app.app_context():?

Summary & Quiz

🎉 Key Takeaways

  • Flask ships without a database on purpose; Flask-SQLAlchemy adds the SQLAlchemy ORM.
  • An ORM maps Python classes to tables, protects against SQL injection, and keeps your code database-agnostic.
  • Configure it with a single SQLALCHEMY_DATABASE_URI, read from an environment variable in production.
  • The application factory creates db unbound, then attaches it with db.init_app(app) — great for testing.
  • db.create_all() builds tables inside an app context but never alters existing ones; schema changes need migrations.

📚 Further Reading

🚀 What's Next?

You have a database wired up but only one throwaway model. Next we go deep on defining database models — column types, constraints, primary keys, and the relationships that connect your tables together.

🎉 Nice work!

Your Flask app can now remember things. Let's design the shapes that memory will take.