Typing support for PyDAL.
This package aims to improve the typing support for PyDAL. By using classes instead of the define_table method,
type hinting the result of queries can improve the experience while developing. In the background, the queries are still
generated and executed by pydal itself, this package only provides some logic to properly pass calls from class methods to
the underlying db.define_table pydal Tables.
TypeDALis the replacement class for DAL that manages the code on top of DAL.TypedTablemust be the parent class of any custom Tables you define (e.g.class SomeTable(TypedTable))TypedFieldcan be used instead of Python native types when extra settings (such asdefault) are required ( e.g.name = TypedField(str, default="John Doe")). It can also be used in an annotation (name: TypedField[str]) to improve editor support over only annotating withstr.TypedRows: can be used as the return type annotation of pydal's.select()and subscribed with the actual table class, so e.g.rows: TypedRows[SomeTable] = db(...).select(). When using the QueryBuilder, aTypedRowsinstance is returned by.collect().
Version 2.0 also introduces more ORM-like functionality. Most notably, a Typed Query Builder that sees your table classes as models with relationships to each other. See 3. Building Queries for more details.
uv pip install typedal
# alternative:
pip install typedalfrom typedal import TypeDAL, TypedTable
db = TypeDAL("sqlite:memory")
# Alternatives:
# db = TypeDAL("sqlite://storage.sqlite")
# db = TypeDAL("postgres://user:password@localhost:5432/mydb")
# db = TypeDAL("mysql://user:password@localhost:3306/mydb")
# ...
@db.define()
class User(TypedTable):
name: str
age: int | None
User.insert(name="Alice", age=30)
adults = User.where(User.age >= 18).collect()
print(adults.column("name")) # ['Alice']If you are new to TypeDAL, start with:
The TypeDAL CLI provides a convenient interface for generating SQL migrations for edwh-migrate from PyDAL or TypeDAL configurations using pydal2sql. It offers various commands to streamline database management tasks.
typedal --help--show-config: Toggle to show configuration details. Default isno-show-config.--version: Toggle to display version information. Default isno-version.--install-completion: Install completion for the current shell.--show-completion: Show completion for the current shell, for copying or customization.--help: Display help message and exit.
cache.clear: Clear expired items from the cache.cache.stats: Show caching statistics.migrations.fake: Mark one or more migrations as completed in the database without executing the SQL code.migrations.generate: Runpydal2sqlbased on the TypeDAL configuration.migrations.run: Runedwh-migratebased on the TypeDAL configuration.setup: Interactively setup a[tool.typedal]entry in the localpyproject.toml.
TypeDAL and its CLI can be configured via pyproject.toml.
See 6. Migrations for more information about configuration.
Below you'll find a quick overview of translation from pydal to TypeDAL.
For more info, see the docs.
| Description | pydal | typedal | typedal alternative(s) | ... |
| Setup |
from pydal import DAL, Field
db = DAL(...) |
from typedal import TypeDAL, TypedTable, TypedField
db = TypeDAL(...) |
||
| Table Definitions |
db.define_table("table_name",
Field("fieldname", "string", required=True),
Field("otherfield", "float"),
Field("yet_another", "text", default="Something")
) |
@db.define
class TableName(TypedTable):
fieldname: str
otherfield: float | None
yet_another = TypedField(str, type="text", default="something", required=False) |
import typing
class TableName(TypedTable):
fieldname: TypedField[str]
otherfield: TypedField[typing.Optional[float]]
yet_another = TextField(default="something", required=False)
db.define(TableName) |
|
| Insert |
db.table_name.insert(fieldname="value") |
TableName.insert(fieldname="value") |
# the old syntax is also still supported:
db.table_name.insert(fieldname="value") |
|
| (quick) Select |
# all:
all_rows = db(db.table_name).select() # -> Any (Rows)
# some:
rows = db((db.table_name.id > 5) & (db.table_name.id < 50)).select(db.table_name.id)
# one:
row = db.table_name(id=1) # -> Any (Row) |
# all:
all_rows = TableName.collect() # or .all()
# some:
# order of select and where is interchangeable here
rows = TableName.select(Tablename.id).where(TableName.id > 5).where(TableName.id < 50).collect()
# one:
row = TableName(id=1) # or .where(...).first() |
# you can also still use the old syntax and type hint on top of it;
# all:
all_rows: TypedRows[TableName] = db(db.table_name).select()
# some:
rows: TypedRows[TableName] = db((db.table_name.id > 5) & (db.table_name.id < 50)).select(db.table_name.id)
# one:
row: TableName = db.table_name(id=1) |
TypeDAL provides some utility functions to interact with the underlying pyDAL objects:
-
get_db(TableName):
Retrieve the DAL instance associated with a given TypedTable or pyDAL Table. -
get_table(TableName):
Access the original PyDAL Table from a TypedTable instance (db.table_name). -
get_field(TableName.fieldname):
Get the pyDAL Field from a TypedField. This ensures compatibility when interacting directly with PyDAL.
These helpers are useful for scenarios where direct access to the PyDAL objects is needed while still using TypeDAL.
An example of this is when you need to do a db.commit() but you can't import db directly:
from typedal.helpers import get_db #, get_table, get_field
MyTable.insert(...)
db = get_db(MyTable)
db.commit() # this is usually done automatically but sometimes you want to manually commit.- Some editors (notably PyCharm) cannot always distinguish class-level and instance-level access on the same symbol.
For example,
Model.somefieldis a field descriptor (query operations like.belongs()), whilemodel.somefieldis the runtime value (for examplelist[str]). TypedFieldlimitations; Since pydal implements some magic methods to perform queries, some features of typing will not work on a typed field:typing.Optionalor a union (Field() | None) will result in errors. The only way to make a typedfield optional right now, would be to setrequired=Falseas an argument yourself. This is also a reason whytyping.get_type_hintsis not a complete solution.