Skip to main content

sqla-lite

A lightweight declarative layer on top of SQLAlchemy that eliminates boilerplate and improves readability.

Python SQLAlchemy

SQLAlchemy is powerful — but model definitions can become verbose and repetitive.

sqla-lite provides a minimal declarative layer that reduces boilerplate, improves readability, and keeps your models clean and maintainable.

No magic. No heavy abstraction. Just less noise.

Want to contribute? See CONTRIBUTING.md.


📦 Installation

Install directly from PyPI:

pip install sqla-lite

sqla-lite already brings its runtime dependency (SQLAlchemy >= 2.0.0).


🚀 Quick Start (The Basics)

Define your models exactly like you would writing pure Python data structures. Use the @table class decorator instead of dealing with explicit inheritance from DeclarativeBase.

You can annotate your attributes by assigning the markers (Id(), Size()) directly!

from sqla_lite import table, Id, Size

@table("users")
class User:
    id: int = Id()             # Automatically setup as Primary Key
    name: str = Size(100)      # VARCHAR(100)
    age: int                   # Automatically inferred as Integer

Automatic Repositories

Say goodbye to the with Session(engine) as session: nightmare. With sqla-lite, you can register a repository to manage your Data Access layer globally for an entity!

from sqla_lite import repository, configure_database

# 1. Define an empty Repository pointing to your Entity
@repository(User)
class UserRepository:
    pass

# 2. Configure your Database globally ONCE (Usually at the start of your application)
from sqlalchemy import create_engine
engine = create_engine("sqlite:///:memory:")

# sqla-lite will generate all tables automatically in Base.metadata (if needed)
from sqla_lite.core import Base
Base.metadata.create_all(engine)

# Inform sqla-lite to use this global engine!
configure_database(engine)

# 3. Use it! No sessions needed!
repo = UserRepository()

user = User(name="John Doe", age=25)
repo.save(user) # Auto-commits

# Search by primary key natively!
john = repo.get(1)
print(john.name)

Simplified Query Methods (@query)

For simple filters, you can avoid manual session boilerplate by using @query. The decorated method receives a query context already bound to self.entity_class.

from sqla_lite import repository, query

@repository(User)
class UserRepository:
    @query
    def find_adults(self, session):
        return session.filter(self.entity_class.age >= 18).all()

If you need full control, you can still use with Session(self.engine) as session: normally.


⚡ Intermediate Usage

Type Inferences and Specific Mappings

sqla-lite understands your annotations and makes reasonable defaults.

  • str automatically becomes String(256) if no Size() is provided.
  • float becomes Float.
  • id: str = Id() automatically generates a UUID when no value is informed.
  • Want highly-precise decimal numbers for currencies? Use the Decimal marker!
from sqla_lite import Decimal

@table("products")
class Product:
    id: int = Id()
    title: str = Size(150)
    
    # 10 digits in total, 2 fractional decimal numbers -> Numeric(10, 2)
    price: float = Decimal(precision=10, scale=2) 
@table("uuid_products")
class UuidProduct:
    id: str = Id()  # Auto-generated UUID if omitted
    title: str = Size(150)

Nullable Control

By default, non-primary-key columns follow SQLAlchemy defaults. If you want explicit control, pass nullable= in column markers:

@table("customers")
class Customer:
    id: int = Id()
    name: str = Size(120, nullable=False)
    credit_limit: float = Decimal(precision=10, scale=2, nullable=True)
    last_contact_at: str = DateFormat("%Y-%m-%d", nullable=True)

Relationship markers also support nullability on generated FK columns:

company: Company = ManyToOne(fields=["tenant_id", "code"], nullable=False)

Default Values

You can define a default value directly in marker properties:

@table("orders")
class Order:
    id: int = Id()
    status: str = Size(40, default="PENDING")
    total: float = Decimal(precision=10, scale=2, default=0)
    due_date: str = DateFormat("%Y-%m-%d", default="2026-12-31")

For simple scalar fields, assigning a literal value also sets a default:

@table("jobs")
class Job:
    id: int = Id()
    retries: int = 3
    title: str = "untitled"

Date Handling

Handling Date strings and casting them into Database Datetime correctly can be a headache. sqla-lite supports both:

  1. Native Python formats: Using datetime.datetime directly.
  2. String Parsing Formats: Use the DateFormat to transparently map python Strings into database DateTime seamlessly!
import datetime
from sqla_lite import DateFormat

@table("events")
class Event:
    id: int = Id()
    
    # Kept as native Datetime everywhere
    created_at: datetime.datetime 
    
    # Allows assigning strings in Python ("27/02/2026"). It'll be saved as a Datetime on DB!
    completed_at: str = DateFormat("%d/%m/%Y")

Example:

evt = Event(
    created_at=datetime.datetime.now(),
    completed_at="27/02/2026"
)
repo.save(evt)

🌋 Advanced Usage

Composite Primary Keys

If your database design demands more complex structures like Many-To-Many resolution tables, or Legacy composite-keys, simply annotate multiple attributes with the Id() marker.

If one of the primary keys is a String and requires a size, you can pass the argument size= into the Id marker.

@table("employee_roles")
class EmployeeRole:
    # Key 1
    employee_id: int = Id()
    # Key 2: String with length!
    role_name: str = Id(size=50) 
    
    assigned_date: datetime.datetime

Querying with Repositories over Composite Keys

You don't need tuples or weird abstractions to retrieve composed key rows via our Repository Pattern. Just pass your identifiers in the sequence they were declared!

@repository(EmployeeRole)
class EmployeeRoleRepo: pass

repo = EmployeeRoleRepo()

# The Repository handles the argument unpacking dynamically
role = repo.get(101, "Software Engineer")
print(f"Loaded Role for Employee {role.employee_id}!")

Relationships (Foreign Keys)

sqla-lite now supports relationship markers for all common cases:

  • ManyToOne (many rows reference one parent)
  • OneToOne (unique reference)
  • OneToMany (list side of one-to-many)
  • ManyToMany (list-to-list through association table)

Table Constraints

You can keep using native SQLAlchemy __table_args__, but now you can also declare constraints directly in @table(...) for better readability.

from sqla_lite import table, Id, ManyToOne, Unique

@table(
    "user_groups",
    constraints=[Unique("user_id", "group_id", name="uq_user_groups_user_id_group_id")],
)
class GroupUser:
    id: str = Id()
    user: User = ManyToOne("id", nullable=False)
    group: Group = ManyToOne("id", nullable=False)
    is_admin: bool = False

If you already have __table_args__, it will continue to work and will be merged with constraints generated by relationship markers.

You can also declare foreign keys at table level with ForeignKey(...):

from sqla_lite import table, Id, ForeignKey

@table(
    "stocks",
    constraints=[ForeignKey("product_id", "products.id", name="fk_stocks_product")],
)
class Stock:
    id: int = Id()
    product_id: int

Check(...) and Index(...) are also supported in the same style:

from sqla_lite import table, Id, Check, Index

@table(
    "jobs",
    constraints=[
        Check("retries >= 0", name="ck_jobs_retries_non_negative"),
        Index("title", name="ix_jobs_title"),
    ],
)
class Job:
    id: int = Id()
    title: str
    retries: int

ManyToOne with simple FK

from sqla_lite import table, Id, Size, Decimal, ManyToOne

@table("products")
class Product:
    id: int = Id()
    title: str = Size(150)
    price: float = Decimal(precision=10, scale=2)

@table("stocks")
class Stock:
    id: int = Id()
    product: Product = ManyToOne(fields="id")

This creates stock.product_id as foreign key to products.id.

ManyToOne with composite FK

Use fields as comma-separated string or list:

from sqla_lite import table, Id, Size, ManyToOne

@table("companies")
class Company:
    tenant_id: int = Id()
    code: str = Id(size=20)
    name: str = Size(100)

@table("employees")
class Employee:
    id: int = Id()
    company: Company = ManyToOne(fields=["tenant_id", "code"])

Equivalent form:

company: Company = ManyToOne(fields="tenant_id,code")

OneToOne

from sqla_lite import table, Id, Size, OneToOne

@table("profiles")
class Profile:
    id: int = Id()
    user_name: str = Size(80)

@table("profile_details")
class ProfileDetail:
    id: int = Id()
    profile: Profile = OneToOne(fields="id")

OneToOne applies a unique constraint on the generated FK columns.

OneToMany

from sqla_lite import table, Id, Size, ManyToOne, OneToMany

@table("parents")
class Parent:
    id: int = Id()
    name: str = Size(80)
    children: list["Child"] = OneToMany(mapped_by="parent")

@table("children")
class Child:
    id: int = Id()
    parent: Parent = ManyToOne(fields="id", back_populates="children")
    title: str = Size(120)

ManyToMany

from sqla_lite import table, Id, Size, ManyToMany

@table("permissions")
class Permission:
    id: int = Id()
    name: str = Size(60)

@table("users")
class User:
    id: int = Id()
    user_name: str = Size(80)
    permissions: list[Permission] = ManyToMany()

An association table is generated automatically.


🔥 Extending Repositories

Because your Repository is a plain Python Class wrapped by @repository, you can implement custom behavior that fits your business logic inside of it. The decorator only injects basic (save, get, delete, find_all) methods, leaving you free to query anything else you like via self.engine:

If you prefer less boilerplate for read operations, you can also use @query here:

from sqla_lite import repository, query

@repository(User)
class UserRepository:
    @query
    def find_adults(self, session):
        return session.filter(self.entity_class.age >= 18).all()

    @query
    def find_by_min_age(self, session, min_age):
        return session.filter(self.entity_class.age >= min_age).all()

If you need full control (joins, custom session lifecycle, explicit transaction boundaries), regular SQLAlchemy session usage still works:

from sqlalchemy.orm import Session

@repository(User)
class UserRepository:
    def find_adults(self):
        with Session(self.engine) as session:
            # self.entity_class holds a reference to the mapped Class!
            return session.query(self.entity_class).filter(self.entity_class.age >= 18).all()

# Usage:
repo = UserRepository()
adults = repo.find_adults()

Created with ❤️. Say goodbye to boilerplate code!

Support this project on Patreon: https://www.patreon.com/cw/ElaraDevSolutions

Release files for sqla-lite 1.0.12

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sqla-lite 1.0.12
File Size Uploaded
sqla_lite-1.0.12.tar.gz 23.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sqla-lite 1.0.12
File Interpreter ABI Platform
sqla_lite-1.0.12-py3-none-any.whl Python 3 none any Details

Total release size: 39.9 kB

Release files / sqla_lite-1.0.12.tar.gz

Download URL sqla_lite-1.0.12.tar.gz
Size 23.9 kB
Tags Source
SHA-256 checksum
How to use checksums
c5b7f90cc7657a6c8d45040d31e40a78c9bd66a336d71ae0eca280615f5a6322
BLAKE2b-256 checksum
How to use checksums
db709ef3949cded34903176b22c6629abd443c5180c0039bd35d708dd94f7cbf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 12, 2026.

Transparency log

Release files / sqla_lite-1.0.12-py3-none-any.whl

Download URL sqla_lite-1.0.12-py3-none-any.whl
Size 16.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
147d370bb215050e10f16ee9c665d054daba55916c37a481e11c09cabec4ad0d
BLAKE2b-256 checksum
How to use checksums
a5cec3d1fd74bc6112a3361e8f0806508d7ff5cb9bd4f85a65a8c5a1c7a09dbf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 12, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.12 This release

2 release files

1.0.9

2 release files

1.0.8

2 release files

1.0.7

2 release files

1.0.6

2 release files

1.0.5

2 release files

1.0.4

2 release files

1.0.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page