ibis-typing
A typed framework for writing Ibis dataframe expressions — with full IDE support, static analysis, and property-based testing.
Ibis is a portable Python dataframe library (DSL) that runs on DuckDB, Polars, Trino, BigQuery, and more. ibis-typing layers a type-safe schema system on top of it, so your transforms carry type information end-to-end.
DBT is a popular SQL transformation framework. **ibis-typing.dbt ** can compile typed Ibis Expressions to DBT SQL for execution in your warehouse, combining the ubiquity of DBT with the expressiveness and benefits of a high-level typed DSL.
Installation
pip install ibis-typing
uv add ibis-typing
After installation, run the type-patch step once to inject typed overloads into your installed ibis package:
python -m ibis_typing.type_patch
Quick start
1. Define input schemas
from attrs import frozen
from ibis_typing import IbisSchema, it
@frozen
class Transaction(IbisSchema):
date: it.Date = None
amount: it.Float64 = None
category: it.String = None
2. Define a typed expression
# my_app.expressions_pkg.monthly_amounts
from attrs import frozen
from collections.abc import Sequence
from ibis_typing import Expression, IbisTable, this, it
from ibis_typing.ibis_extension_method import TableMethod, ValueMethod
from ibis import Table, ir, literal
@frozen
class MonthlyAmounts(Expression):
@classmethod
def from_expression(cls, inputs: IbisTable[Transaction]):
cols = inputs.cols
table = (
inputs.table
@ AggregateByMonth(cols.date, sums=[cols.amount])
@ it.deferred.filter(this[cols.amount] != literal(0))
)
return cls.of(table)
@frozen
class AggregateByMonth(TableMethod):
date: it.Date
sums: Sequence[it.Float64]
def apply(self, table: Table) -> Table:
return (
table
@ it.Select(expr={"month": this[self.date] @ StartOfMonth()})
@ it.Aggregate(by=["month"], sum=self.sums)
)
@frozen
class StartOfMonth(ValueMethod[ir.DateValue, ir.DateValue]):
def apply(self, value: ir.DateValue):
return value @ it.defer(ir.DateValue).truncate("M")
3. Generate IbisSchema classes for Expression classes
Generate IbisSchema classes for the output of Expression classes, and write them to .py files:
python3 -m ibis_typing.schemagen \
my_module.expressions_pkg \
--schema-package my_app.generated.schemas \
--schema-suffix Schema \
--write
Resulting files will contain IbisSchema classes like this:
# my_app.generated.schemas.monthly_amounts_schema
from attrs import frozen
from ibis_typing.ibis_types import *
@frozen
class MonthlyAmountsSchema(IbisSchema):
month: Date = None
amount: Float64 = None
Update your Expression classes to use the generated IbisSchema for their output schema:
from my_app.generated import schemas
@frozen
class MonthlyAmounts(schemas.MonthlyAmountsSchema, Expression):
...
Tip: Create a regression test that checks for unexpected schema changes, and update expected schemas when intentional changes are made:
# my_app.tests.test_generate_schemas
def test_generate_ibis_expression_schema_packages(update_expected):
expr_schema_pkgs = {
samples: sample_schemas,
}
expr_to_schema_package = {
expr: schema_pkg
for expr_pkg, schema_pkg in expr_schema_pkgs.items()
for expr in schema_writer.list_expressions_in_package(expr_pkg)
}
schema_writer.generate_schemas_with_diff_check(
expr_to_schema_package, update_expected
)
4. Test with Hypothesis
pytest fixtures are registered automatically — no conftest.py needed.
from hypothesis import given, strategies as st
from ibis_typing import utils
from ibis_typing.hypothesis import strategy_for
@given(transactions=st.lists(strategy_for(Transaction), min_size=1))
def test_monthly_amounts(evaluate_table, transactions):
reference_output = utils.group_by(transactions, key=lambda t: t.date.replace(day=1))
monthly_amounts = [
MonthlyAmounts(month=month, amount=sum(t.amount for t in month_transactions))
for month, month_transactions in reference_output.items()
]
# Get evaluated expression rows together with expected, both as sorted lists
actual, expected = evaluate_table(MonthlyAmounts, [*transactions, *monthly_amounts])
assert actual == expected
5. Evaluate against a backend
from datetime import date
from ibis_typing import IbisConnection, evaluator
from ibis_typing.table_store import ParquetTableStore
conn = IbisConnection() # defaults to in-memory DuckDB
transactions = Transaction.of_rows( # give test data via IbisSchema rows.
[Transaction(date=date(2024, 1, 15), amount=100.0, category="A")]
)
monthly_amounts = evaluator.from_expression(MonthlyAmounts, transactions)
results: list[MonthlyAmounts] = list(conn.fetch_table(monthly_amounts))
# Write and read parquet files locally, stored by schema name.
from pathlib import Path
store = ParquetTableStore(Path("/tmp/table_store"))
store.write_table(transactions)
table = store(Transaction)
rows: list[Transaction] = list(conn.fetch_table(table))
Core concepts
graph TD
IbisSchema -->|describes| IbisTable
IbisTable -->|In| Expression
Expression -->|Out| IbisTable
TableProvider -->|provides tables to| Expression
IbisConnection -->|fetches typed rows from| IbisTable
ChecksumBuckets -->|incremental inputs to| IncrementalExpression
IncrementalExpression -->|is a| Expression
BucketedInputsExpression -->|is a| IncrementalExpression
RevertibleTableExpression -->|can revert| Expression
| Class | Purpose |
|---|---|
IbisSchema |
Base class for typed table schemas (attrs frozen dataclass) |
IbisTable[S] |
Generic typed wrapper around ibis.Table |
Expression |
Abstract base for typed ibis transforms |
TableMethod |
Extension method on ibis.Table returning another Table |
ValueMethod |
Extension method on ibis.Value returning another Value |
Deferred |
table @ it.deferred.distinct() value @ it.defer().notnull() |
IbisConnection |
Typed backend wrapper: fetch_table(), evaluate(), read/write_parquet() |
BucketedInputsExpression |
Expression that only re-runs for changed input buckets |
ChecksumBuckets |
Checksum-based incremental input tracking |
RevertibleTableExpression |
Transform that can undo itself back to the original schema |
Type aliases
Declare schema fields using column-type aliases from ibis_typing.it:
from ibis_typing import it
it.Int8, it.Int16, it.Int32, it.Int64
it.Float32, it.Float64
it.Boolean
it.String, it.Binary
it.Decimal
it.Date, it.Time, it.Timestamp
it.UUID, it.JSON
it.Array[it.Int64]
it.Map[it.String, it.Float64]
it.Struct[MyTypedDict]
Table operations
Use the infix @ operator for composable, typed table transforms via TableMethod.
Standard Ibis Table methods are available via it.deferred.distinct().
Ibis Value methods are available via e.g. it.defer(type_=ir.Value).notnull().
from ibis_typing import IbisSchema, IbisTable, this, it
@frozen
class InputSchema(IbisSchema):
a: it.Float64 = None
b: it.Float64 = None
category: it.String = None
amount: it.Float64 = None
key: it.String = None
inputs: IbisTable[InputSchema] = ...
other_table: IbisTable = ...
cols = InputSchema.cols
table = (
inputs.table
@ it.Select(cols.a, cols.b, expr={"c": this[cols.a] + this[cols.b]})
@ it.Aggregate(by=[cols.category], sum=[cols.amount])
@ it.InnerJoin(other_table.table, keys=[cols.key])
@ it.deferred.filter(this[cols.amount] != 0)
)
Time series features
ibis_typing.time_series adds trailing-window features, such as sums, standard deviations and the median frequency, to every row of a series. Each row keeps its columns and gets one new column per value column, feature and window size.
from datetime import date
from attrs import frozen
from ibis_typing import IbisSchema, it
from ibis_typing.time_series import TimeSeriesFeatureExtraction
from ibis_typing.time_series.features import MeanAbsDiff, Sum
@frozen
class Balance(IbisSchema):
tenant: it.String = None
month: it.Date = None
cash: it.Float64 = None
balances = Balance.of_rows(
[
Balance(tenant="a", month=date(2024, 1, 1), cash=10.0),
Balance(tenant="a", month=date(2024, 2, 1), cash=12.0),
]
)
cols = Balance.cols
table = balances.table @ TimeSeriesFeatureExtraction(
keys=[cols.tenant],
order_by=cols.month,
columns=[cols.cash],
features=[Sum(), MeanAbsDiff()],
windows=[2],
window_unit="months",
)
# Adds cash__sum_last_2_months and cash__mean_abs_diff_last_2_months
Pytest fixtures
The following fixtures are auto-registered via the pytest plugin entry point (no conftest.py needed):
| Fixture | Purpose |
|---|---|
evaluate_table |
Runs an Expression, returns (actual, expected) row lists |
fetch_table |
Fetches rows from an IbisTable |
ibis_connection |
Provides a IbisConnection for relevant DB backends |
Extras
ibis_typing.type_patch— patches installed ibis with typed@overloadstubs foribis.ifelse,ibis.cases,ibis.coalesce, etc.ibis_typing.schema_writer— code-gen: writeIbisSchema.pyfiles fromExpressionoutput schemasibis_typing.plot— plots the dependency graph of anExpressionusing matplotlib/graphvizibis_typing.custom— custom ibis operations:DateAddMonth,DateAddDay,ColumnChecksum,JsonParse,JsonFormat,UUIDFromInt,LuhnCheck
Contributing
git clone https://github.com/FortnoxAB/ibis-typing
cd ibis-typing
make
Pull requests welcome. Please run make before submitting.
License
Metadata
Release files for ibis-typing 1.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| ibis_typing-1.2.0.tar.gz | 78.7 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ibis_typing-1.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 198.0 kB
Release files / ibis_typing-1.2.0.tar.gz
| Download URL | ibis_typing-1.2.0.tar.gz |
|---|---|
| Size | 78.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bd93535cd9f4400d742e2c91b975ca069983f034559484bcff77331876db78cd
|
|
BLAKE2b-256 checksum How to use checksums |
3e1d1e2dc95a3141ecdd28067dd9cdac6093f212675e704e04d242323cfba5c7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / ibis_typing-1.2.0-py3-none-any.whl
| Download URL | ibis_typing-1.2.0-py3-none-any.whl |
|---|---|
| Size | 119.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8cfbc345b8a68d51b2976476015c78d227ce0c1faa4c9489e5cd949a2fcb4c6e
|
|
BLAKE2b-256 checksum How to use checksums |
11d99558238272a338960ee21b068dac3c093805dec91ae3652cd133bf7f9010
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for ibis-typing 1.2.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.