Skip to content

Installation

pip install snake-orm==0.1.0b1   # or: pip install --pre snake-orm

The version is pinned because it is a beta

pip install snake-orm on its own installs NOTHING: pip does not pick up a preliminary version unless it is asked for by name or with --pre. That is the point of publishing a beta — nobody upgrades into it by accident while the API is still moving.

pip install snakeorm does not work either, and the two names are not a typo. pyproject.toml declares name = "snake-orm" — the distribution name, the one you install — while the package you import is snakeorm, the import name. The whole story is in the release process.

From a checkout of the repository, to work on the ORM itself:

uv sync --all-extras --all-groups   # from the root of the checkout

Needs Python 3.11+: deep typing uses dataclass_transform (PEP 681) and the X | None syntax in annotations.

Your engine's driver

Three engines, all three first class: PostgreSQL, MySQL/MariaDB and SQLite. You pair a dialect with a driver; only MySQL needs an extra install:

# nothing to install: psycopg2-binary is a dependency of snakeorm
from snakeorm import PostgresDialect, PsycopgDriver

driver = PsycopgDriver.connect("postgresql://user:pass@localhost/mydb")
dialect = PostgresDialect()
uv sync --extra mysql     # brings PyMySQL
from snakeorm import MySQLDialect, PyMySQLDriver

driver = PyMySQLDriver.connect(
    host="localhost", user="user", password="pass", database="mydb"
)
dialect = MySQLDialect()

MySQL takes connection arguments, not a one-piece DSN. That's the engine's shape, and the driver doesn't invent a DSN parser to hide it.

PyMySQL is pure Python, so it installs anywhere without a C toolchain. mysqlclient is faster and works just as well: it speaks the same %s placeholders, so the dialect does not change.

# nothing to install: sqlite3 ships in the stdlib
from snakeorm import SQLiteDialect, SQLiteDriver

driver = SQLiteDriver.connect("./my.db")  # or ":memory:"
dialect = SQLiteDialect()

The same, asynchronously

Generating SQL has no color — it doesn't execute — so the dialect doesn't change. Only the driver does, and the session you pair it with (AsyncSession instead of SnakeSession):

uv sync --extra async     # brings psycopg 3
from snakeorm import AsyncPsycopgDriver, PostgresDialect

driver = await AsyncPsycopgDriver.connect("postgresql://user:pass@localhost/mydb")
dialect = PostgresDialect()
uv sync --extra mysql     # the SAME extra as the synchronous path
from snakeorm import AsyncPyMySQLDriver, MySQLDialect

driver = await AsyncPyMySQLDriver.connect(
    host="localhost", user="user", password="pass", database="mydb"
)
dialect = MySQLDialect()
# nothing to install here either
from snakeorm import AsyncSQLiteDriver, SQLiteDialect

driver = await AsyncSQLiteDriver.connect("./my.db")  # or ":memory:"
dialect = SQLiteDialect()

There is no extra just for asynchronous MySQL

Two extras buy a driver: async (psycopg 3) and mysql (PyMySQL). Only PostgreSQL has a native asynchronous driver, and that is what async buys. MySQL and SQLite serve their synchronous driver from a thread of their own, so they need nothing beyond what you already installed.

Why the driver is your job

They are two distinct axes: the dialect decides how the SQL is WRITTEN (placeholders, quoting, LIMIT); the driver decides how it is EXECUTED. Making them parameters and not a magic factory is what lets you wrap the driver with a logger or a pool without the rest noticing. See dialects.

Configuration

Connections are read from environment variables or a .env:

DATABASE_URL=postgresql://user:pass@localhost/mydb

SNAKEORM_DSN and the classic DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD also work. For several databases at once, see multiple databases.

Check that it works

from snakeorm import PostgresDialect, PsycopgDriver, SnakeSession, SnakeRow, snake_row

@snake_row
class Version(SnakeRow):
    value: str

dsn = "postgresql://user:pass@localhost/mydb"
session = SnakeSession(PsycopgDriver.connect(dsn), PostgresDialect())
print(session.raw("SELECT version() AS value", into=Version)[0].value)

The tools this project takes for granted

Everything that sets this ORM apart lives in the type-checker. Both live in the dev group, so the sync above already brought them:

uv run mypy .          # must pass
uv run pyright         # and agree with mypy

Without them, the deep typing that sets this ORM apart is invisible.


Next: your first model.