Basic Usage

Adding Models

from pydantic import BaseModel
from pydantic_sqlite import DataBase

class Product(BaseModel):
    uuid: str
    name: str
    price: float
    in_stock: bool

db = DataBase()

# Create and add a product
product = Product(
    uuid="prod-001",
    name="Laptop",
    price=999.99,
    in_stock=True
)
db.add("Products", product)

# Add more products
db.add("Products", Product(uuid="prod-002", name="Mouse", price=29.99, in_stock=True))
db.add("Products", Product(uuid="prod-003", name="Keyboard", price=79.99, in_stock=False))

Querying Models

Querying all entries

# Iterate through all products
for product in db("Products"):
    status = "Available" if product.in_stock else "Out of stock"
    print(f"{product.name}: ${product.price} ({status})")

Querying with Conditions

Use the where parameter to filter results:

# Get all products in stock
for product in db("Products", where="in_stock = :stock", where_args={"stock": True}):
    print(f"{product.name} is available")

# Get products over $50
for product in db("Products", where="price > :min_price", where_args={"min_price": 50}):
    print(f"{product.name}: ${product.price}")

# Complex queries
for product in db(
    "Products",
    where="price > :min_price AND in_stock = :stock",
    where_args={"min_price": 50, "stock": True}
):
    print(product.name)

Primitive Datatypes

pydantic-sqlite supports all common Python types. Here's a comprehensive example with all basic primitive types:

from typing import Optional
from pydantic import BaseModel
from pydantic_sqlite import DataBase

class DataExample(BaseModel):
    uuid: str
    name: str
    count: int
    rating: float
    active: bool
    description: Optional[str] = None
    notes: Optional[str] = None
    categories: list[str]

db = DataBase()

# Add one example with all fields
db.add("Examples", DataExample(
    uuid="ex-1",
    name="Complete Example",
    count=42,
    rating=4.5,
    active=True,
    description="A complete entry",
    notes=None,  # This optional field is None
    categories=["python", "database", "tutorial"]
))

# Retrieve and verify all types
for item in db("Examples"):
    assert isinstance(item.uuid, str)
    assert isinstance(item.name, str)
    assert isinstance(item.count, int)
    assert isinstance(item.rating, float)
    assert isinstance(item.active, bool)
    assert isinstance(item.categories, list)
    # Optional fields can be None or their type
    assert item.notes is None
    assert item.description == "A complete entry"

    print(f"{item.name}: {item.rating}★")
    print(f"  Description: {item.description}")
    print(f"  Categories: {', '.join(item.categories)}")

Supported Field Types

The following field types are supported and can be stored and loaded through a Pydantic model:

Field type Notes
str, int, float, bool Stored as SQLite scalar values
datetime, Decimal Serialized by sqlite-utils
Optional[T] Supports None values
Union[...] Reconstructed using the first matching type
Literal[...] Stored as a string
list[T] Primitive values are stored as JSON
tuple[T, ...] Stored as JSON and reconstructed as a tuple
set[T] Stored as JSON and reconstructed as a set
dict[K, V] Primitive values are stored as JSON
Enum and StrEnum The Enum value is stored and the Enum member is reconstructed
Nested BaseModel Requires a foreign_tables entry; see Nested Models. Use (table, pk) tuples when the nested model's PK is not "uuid".

Any is also supported for storage. Since it carries no concrete type information, an Enum stored through an Any field is loaded as its value rather than as an Enum member. Enum fields should therefore be explicitly typed when the Enum type must be preserved.

Enum Values

Enum fields are stored using their values and reconstructed as Enum members when queried:

from enum import Enum
from pydantic import BaseModel
from pydantic_sqlite import DataBase

class Status(str, Enum):
    ACTIVE = "active"
    INACTIVE = "inactive"

class User(BaseModel):
    uuid: str
    name: str
    status: Status

db = DataBase()
db.add("Users", User(uuid="user-1", name="Alice", status=Status.ACTIVE))

user = db.model_from_table("Users", "user-1")
assert user.status is Status.ACTIVE
assert isinstance(user.status, Status)

The same behavior applies to Enum fields with integer values, such as class Priority(Enum): LOW = 1.

Updating Data

pydantic-sqlite uses an "upsert" strategy. If you add a model with the same primary key, it updates the existing entry:

from pydantic import BaseModel
from pydantic_sqlite import DataBase

class User(BaseModel):
    uuid: str
    username: str
    email: str

db = DataBase()

# Initial user
user = User(uuid="user-1", username="alice", email="alice@example.com")
db.add("Users", user)

# Update: add the same UUID with new data
updated_user = User(uuid="user-1", username="alice", email="alice.new@example.com")
db.add("Users", updated_user)

# Only one entry with uuid="user-1", with the new email
for user in db("Users"):
    print(user.email)  # alice.new@example.com

Checking for Existence

# Check by primary key value (uuid)
if db.model_in_table("Users", "user-1"):
    print("User exists")

# Check by model instance
if db.model_in_table("Users", user):
    print("Alice is in the database")

# Get a specific model by uuid
user = db.model_from_table("Users", "user-1")
if user:
    print(f"Found: {user.username}")

Deleting Data

Remove rows from the database with delete and delete_where:

from pydantic import BaseModel
from pydantic_sqlite import DataBase

class Product(BaseModel):
    uuid: str
    name: str
    in_stock: bool

db = DataBase()

laptop = Product(uuid="prod-001", name="Laptop", in_stock=True)
db.add("Products", laptop)
db.add("Products", Product(uuid="prod-002", name="Mouse", in_stock=True))
db.add("Products", Product(uuid="prod-003", name="Keyboard", in_stock=False))

# Delete all rows matching a condition -> number of removed rows
removed = db.delete_where("Products", where="in_stock = :stock", where_args={"stock": False})
print(removed)  # 1 (the Keyboard)

# Delete a single row by primary key -> True if the row was deleted
deleted = db.delete("Products", "prod-001")
print(deleted)  # True

# Delete by model instance (the primary key is extracted automatically)
deleted = db.delete("Products", laptop)
print(deleted)  # False - the Laptop row was already deleted above

# Un-deleted rows are still there
for product in db("Products"):
    print(product.name)  # Mouse

delete and delete_where raise a KeyError if the table does not exist.

Note: deleting only removes rows from the specified table. Rows in foreign tables that are referenced by nested models are kept. See Cascading Deletes to delete nested rows as well.

Persisting to Disk

There are two ways to work with a file-based database:

Pass a filename (or Path) to DataBase. The database is opened directly, all changes are written to the file immediately and existing data (including table metadata) is loaded automatically:

from pydantic_sqlite import DataBase

db = DataBase("my_database.db")

# ... add and query data - everything is persisted automatically ...

# Close the underlying connection when done
db.close()

If the file already exists, its tables and metadata are hydrated on initialization, so previously stored models can be queried right away:

db = DataBase("my_database.db")
for person in db("Persons"):
    print(person)

Note: DataBase.close() closes the underlying SQLite connection and can be called multiple times safely.

2. In-memory database + save()/load()

By default, DataBase() (no filename) creates an in-memory database. Use save() to dump it to a file and load() to read a file back into memory:

from pydantic_sqlite import DataBase

# In-memory database
db = DataBase()

# ... add and query data ...

# Save to disk
db.save("my_database.db")

# Later: load the file into a new in-memory database
db2 = DataBase()
db2.load("my_database.db")
for person in db2("Persons"):
    print(person)

Calling save() on an already file-backed database is a no-op: it only logs a warning that the data is already stored in the file.

Note: When loading (or reopening) an existing database file, the Pydantic model classes are resolved by their module name. Make sure the models are defined in importable modules (or registered beforehand), otherwise the affected tables are skipped with a warning.