SQLAlchemy OperationalError: no such table (tested)

sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such table means the database you connected to does not contain that table. We reproduced four causes: create_all() never ran, it ran before the models were imported, a relative sqlite:/// path opened a different file, and an in-memory database seen through a second connection.

Blocks 1-4 import this models.py:

from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase): pass
class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)
    email: Mapped[str] = mapped_column(String)

1. The error

from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
from models import User
engine = create_engine("sqlite:///app.db")       # tables were never created
with Session(engine) as s:
    s.scalars(select(User)).all()

Output (last lines of the traceback):

    ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such table: users
[SQL: SELECT users.id, users.email 
FROM users]
(Background on this error at: https://sqlalche.me/e/21/e3q8)

SQLAlchemy never creates tables on its own. Declaring a model only describes the table; create_engine("sqlite:///app.db") creates an empty file on first connect. Until something runs Base.metadata.create_all(engine) or a migration (alembic upgrade head, flask db upgrade), the table does not exist.

2. create_all ran, but before the models were imported

from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase): pass
from sqlalchemy.orm import Mapped, mapped_column
from base import Base
class Post(Base):
    __tablename__ = "posts"
    id: Mapped[int] = mapped_column(primary_key=True)
from sqlalchemy import create_engine, inspect
from base import Base
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)          # post.py not imported yet: Base knows no tables
print("before import:", inspect(engine).get_table_names())
import post                               # registering the model adds it to Base.metadata
Base.metadata.create_all(engine)
print("after import: ", inspect(engine).get_table_names())

Output:

before import: []
after import:  ['posts']

create_all only creates tables registered on Base.metadata at the moment it runs, and a model registers itself when its module is imported. In apps that split models into files, import every model module before calling create_all; otherwise it silently creates nothing.

3. A relative sqlite:/// path points somewhere else

import os
from sqlalchemy import create_engine, inspect
from models import Base
os.makedirs("/w/project", exist_ok=True); os.chdir("/w/project")
engine = create_engine("sqlite:///app.db")
Base.metadata.create_all(engine)                 # creates /w/project/app.db
os.chdir("/w")                                   # e.g. run from another directory, or by cron/systemd
engine2 = create_engine("sqlite:///app.db")      # relative: a NEW empty /w/app.db
print(engine2.url.database, "->", os.path.abspath(engine2.url.database))
print("tables:", inspect(engine2).get_table_names())

Output:

app.db -> /w/app.db
tables: []

sqlite:///app.db (three slashes) is relative to the current working directory, not to your script. Run from another directory, by cron, systemd, a test runner or an IDE, and SQLite silently creates a new empty app.db there. Print engine.url.database and its absolute path when debugging.

4. In-memory SQLite with more than one connection

from sqlalchemy import create_engine, inspect, text
from sqlalchemy.pool import QueuePool
from models import Base
# in-memory SQLite: every new connection is a brand-new empty database
engine = create_engine("sqlite://", poolclass=QueuePool)
with engine.connect() as c1, engine.connect() as c2:
    Base.metadata.create_all(c1); c1.commit()
    print("c1:", inspect(c1).get_table_names())
    print("c2:", inspect(c2).get_table_names())

Output:

c1: ['users']
c2: []

Each connection to sqlite:// (in-memory) is a separate, empty database. Tables created on one connection are missing on another. By default SQLAlchemy keeps one in-memory connection per thread (SingletonThreadPool), so a forced pool or a second thread (common in tests and web servers) triggers this. Use a file, or poolclass=StaticPool with connect_args={"check_same_thread": False} so every session shares one connection.

5. Fix: absolute path, then create_all (or migrate)

from pathlib import Path
from sqlalchemy import create_engine, inspect
from models import Base
DB = Path(__file__).resolve().parent / "data.db"     # absolute, independent of the cwd
engine = create_engine(f"sqlite:///{DB}")
Base.metadata.create_all(engine)                     # no-op for tables that already exist
print(engine.url.database)
print("tables:", inspect(engine).get_table_names())

Output:

/w/data.db
tables: ['users']

Build the path from __file__ (four slashes in the URL for an absolute path, as printed above), import all models, then call create_all; it skips tables that already exist. For a database that already has data and needs schema changes, use Alembic migrations instead.

Related: sqlite3 no such table (3 causes) · SQLAlchemy IntegrityError · SQLAlchemy DetachedInstanceError.

Frequently asked questions

Does SQLAlchemy create tables automatically?

No. Call Base.metadata.create_all(engine) after importing all models, or run your migrations (Alembic / flask db upgrade).

Why does create_all not create my table?

The model's module was not imported before create_all ran, so the table was not registered on Base.metadata. Import all model modules first.

Why does it work in one directory but not another?

sqlite:///app.db is relative to the current working directory. Build an absolute path from __file__.

Written with AI assistance. Measured on 2026-10-23 with SQLAlchemy 2.1.6 and Python 3.14.7 (python:3-alpine in Docker). Each block was run verbatim and the output pasted unedited (block 1 shows the last 5 lines of the traceback). PlainTables is our extension; Python and SQLAlchemy are free.