Asyncio Support
SQLiter provides optional async support through sqliter.asyncio and sqliter.asyncio.orm.
Install the async extra first:
uv add 'sqliter-py[async]'
Async Database
Use AsyncSqliterDB for async CRUD and query execution:
from sqliter.asyncio import AsyncSqliterDB
from sqliter.model import BaseDBModel
class User(BaseDBModel):
name: str
async def main() -> None:
db = AsyncSqliterDB("example.db")
await db.create_table(User)
user = await db.insert(User(name="Ada"))
fetched = await db.get(User, user.pk)
users = await db.select(User).filter(name="Ada").fetch_all()
await db.delete(User, user.pk)
await db.close()
You can also use the async context manager:
async with AsyncSqliterDB("example.db") as db:
await db.create_table(User)
Async Queries
AsyncQueryBuilder keeps the same chain-building style as the sync query API, but terminal operations are awaited:
results = await (
db.select(User)
.filter(name="Ada")
.order("name")
.limit(10)
.fetch_all()
)
These query terminal methods are async:
fetch_all()fetch_one()fetch_first()fetch_last()fetch_dicts()count()exists()update()delete()
Async query builders also support the same chain methods as sync, including fields(), only(), and aggregate filtering via having().
AsyncSqliterDB.get_table_names() is a method rather than a property because async properties cannot be awaited.
Async ORM
For async ORM usage, define models with AsyncBaseDBModel and the async relationship descriptors:
from sqliter.asyncio import AsyncSqliterDB
from sqliter.asyncio.orm import AsyncBaseDBModel, AsyncForeignKey
class Author(AsyncBaseDBModel):
name: str
class Book(AsyncBaseDBModel):
title: str
author: AsyncForeignKey[Author] = AsyncForeignKey(
Author,
related_name="books",
on_delete="CASCADE",
)
Forward Foreign Keys
Async forward foreign keys use explicit lazy loading:
book = await db.get(Book, 1)
author = await book.author.fetch()
If the relationship was eager loaded with select_related(), the related model instance is available directly:
book = await db.select(Book).select_related("author").fetch_one()
print(book.author.name)
Reverse Relationships
Reverse relationships return async query wrappers:
author = await db.get(Author, 1)
books = await author.books.fetch_all()
count = await author.books.count()
If reverse relationships are prefetched, the same async read methods still work:
author = await db.select(Author).prefetch_related("books").fetch_one()
books = await author.books.fetch_all()
Many-to-Many
Async many-to-many relationships return async managers:
from sqliter.asyncio.orm import AsyncManyToMany
class Tag(AsyncBaseDBModel):
name: str
class Article(AsyncBaseDBModel):
title: str
tags: AsyncManyToMany[Tag] = AsyncManyToMany(
Tag,
related_name="articles",
)
article = await db.insert(Article(title="Guide"))
tag = await db.insert(Tag(name="python"))
await article.tags.add(tag)
tags = await article.tags.fetch_all()
Available async many-to-many operations include:
fetch_all()fetch_one()count()exists()filter()add()remove()clear()set()
Differences From Sync ORM
The main intentional difference is foreign-key lazy loading:
- Sync ORM:
post.author.name - Async ORM lazy loading:
author = await post.author.fetch()
This difference is required because Python attribute access cannot implicitly await.
mypy and Static Type Checking
Two areas require explicit cast() calls when using strict mypy.
FK lazy loading
AsyncForeignKey is typed to return the related model type (T) so that eager-loaded access — book.author.name after select_related() — type-checks without any extra annotation. At runtime the lazy-loaded value is an AsyncLazyLoader[T], not T itself. The two return types cannot both be expressed accurately as a single overload without breaking one of the two use cases, so the eager-loading path was chosen as the ergonomic default.
When using --strict mypy, lazy FK access requires a cast:
from typing import cast
from sqliter.asyncio.orm import AsyncLazyLoader
# mypy sees book.author as Author, not AsyncLazyLoader[Author]
loader = cast(AsyncLazyLoader[Author], book.author)
author = await loader.fetch()
Without --strict (or with # type: ignore[union-attr]) you can write the simpler form directly:
author = await book.author.fetch()
Reverse FK accessors
Reverse relationship accessors (author.books, post.tags, etc.) are installed dynamically via setattr, so AsyncBaseDBModel.__getattribute__ returns object for them. Under strict mypy, cast to the appropriate manager type:
from typing import cast
from sqliter.asyncio.orm import AsyncReverseQuery
books_query = cast(AsyncReverseQuery, author.books)
books = cast(list[Book], await books_query.fetch_all())
Note
These are mypy-only workarounds. At runtime book.author is always an AsyncLazyLoader (lazy) or the model instance (eager), and author.books is always an AsyncReverseQuery. No cast is needed if you are not running strict type checking.