Postgres · Typed · Fast

Postgres in Python. Typed and fast.

Define your tables as Python models. Get fully typehinted SELECTs, INSERTs, and migrations, running close to raw-protocol speed. Common queries are easy. Complex ones stay possible.

models.pypython
from iceaxe import TableBase, Field, select
 
class Person(TableBase):
id: int = Field(primary_key=True)
name: str
age: int
 
adults = await conn.exec(
select(Person).where(Person.age >= 21)
)
§ 01 · The origin

We wanted an ORM that felt like the rest of modern Python. Typed, explicit, fast.

Most ORMs abstract over every database at once: Postgres, MySQL, SQLite, all behind one interface. You rarely use that portability; almost nobody swaps databases mid-project. You pay for it anyway. Async gets bolted onto a synchronous core. Rows come back in bespoke types that don't act like the Python around them.

Iceaxe's philosophy is the opposite. It assumes Postgres, the database most webapps already run on, and treats async and types as the default rather than an afterthought. The 99% of queries that are plain SELECTs, INSERTs, and UPDATEs are trivial and fully typed. The rare complex query drops to SQL you can read, still typed on the way out. Easy things easy, hard things possible.

Common queries· easy

Trivially, and fully typed

SELECT, INSERT, and UPDATE as plain Python objects. No boilerplate, no stringly-typed columns.

adults = await conn.exec(
    select(User).where(User.age >= 21)
)
# → list[User], fully typed
Complex queries· possible

Raw SQL, still typed

Drop to SQL you can read for the hard 1%, with proper type casting so results stay safe.

query = f"SELECT count(*) AS total FROM {sql(User)}"
totals = await conn.exec(
    select(alias("total", int)).text(query)
)
# → list[int], cast straight to a primitive
01 · Typed queries

select() returns the right types.

Every query is statically checked by mypy and pyright. Reference a column that does not exist and it fails in your editor, not at runtime. Results come back as real Python objects, not dict[str, Any]. Even for complex joins and filtering.

02 · Built for speed

Close to raw-protocol throughput.

Iceaxe sits on asyncpg with batched operations, connection pooling, and lean query building. It aims to match the fastest ORMs in Python and get out of the way of Postgres.

03 · Postgres-native

Leans into Postgres, not around it.

No lowest-common-denominator abstraction over five databases. Iceaxe targets Postgres directly, so native features are first-class and the generated SQL stays predictable.

04 · Migrations included

Change a model, get a migration.

Generate migrations from your table definitions, review the SQL, and roll forward or back. Your schema is versioned alongside the code that depends on it.

For Postgres builders

When dict[str, Any] stops being good enough.

It starts clean. A few asyncpg calls and a couple of helpers. Then the queries multiply. Every result is a dict[str, Any] you hope you spelled right. One renamed column slips through to production. Iceaxe gives you that same direct line to Postgres, now with your models as the source of truth. The column that no longer exists is an error in your editor, not a 2am page.

quickstartshell
$ uv add iceaxe
$ docker run -p 5432:5432 postgres:16
connect a DBConnection and query
One package, one database

Add it and go.

Iceaxe ships as a single Python package with no required services beyond the Postgres you already run. Define a TableBase, open a DBConnection, and start querying with full types. When you're ready to deploy to production, we have you covered too.

Stop guessing what your queries return.

Iceaxe types every row, from schema to result.

Stay in the loop

Get the updates.

New features, release notes, and type-driven Postgres deep dives for Iceaxe. Occasional, no spam.

No spam. Unsubscribe anytime.