Skip to main content

IceFrame (Alpha)

A DataFrame-like library for working with Apache Iceberg tables using REST catalogs with local execution.

IceFrame provides a simple, intuitive API for creating, reading, updating, and deleting Iceberg tables, as well as performing maintenance operations and exporting data.

IceFrame 0.14.0 requires Python 3.10+, upgrades the optional MCP server to the official Python SDK 2.1 protocol stack, and completes full-package static type checking. It also includes the correctness, UX, packaging, and performance hardening listed in CHANGELOG.md.

Features

  • DataFrame API: Familiar interface for working with tables — read_table, to_arrow, to_pandas, lazy, head, describe, count_rows, scan_batches
  • Local Execution: Uses PyIceberg, PyArrow, and Polars for efficient local processing
  • Catalog Support: REST catalogs (Dremio, Tabular, Polaris, …) with credential vending, plus PyIceberg's sql (SQLite/Postgres), memory, glue, hive, and dynamodb catalogs by pass-through
  • CRUD Operations: Create, Read, Update, Delete tables and data
  • Native upserts and transactions: ice.upsert(...) on PyIceberg's atomic Table.upsert, and with ice.transaction(tbl) as txn: for multi-operation atomic commits
  • Predicate, projection and limit pushdown: the query builder pushes WHERE, SELECT and LIMIT into the Iceberg scan when it is sound to do so, and re-applies anything it cannot push locally so results stay correct
  • Metadata tables: ice.inspect(tbl).snapshots() / .files() / .partitions() / .manifests() / .history() / .refs() as Polars frames
  • Maintenance: Expire snapshots, remove orphan files (dry-run by default), compact data files (bin-pack, sort, approximate z-order)
  • Data quality: constraint validation where nulls fail by default, wired into append_to_table(validators=[...]) as a write gate
  • Typed: ships py.typed, so annotations are visible to downstream type checkers
  • Structured logging: standard logging throughout the library; no print() to stdout
  • Export: Export data to Parquet, CSV, and JSON

Documentation

Getting Started

Data Ingestion

Querying & Processing

Table Management

Maintenance & Quality

Advanced Features

Recipes

Installation

IceFrame requires Python 3.10 or newer and supports Python 3.10-3.13.

pip install iceframe

For cloud storage support:

pip install "iceframe[aws]"   # AWS S3
pip install "iceframe[gcs]"   # Google Cloud Storage
pip install "iceframe[azure]" # Azure Data Lake Storage

Install every optional runtime integration with pip install "iceframe[all]". For a smaller environment, prefer only the extras listed in Dependencies.

Quick Start

  1. Create a .env file with your catalog credentials (see .env.example):
ICEBERG_CATALOG_URI=https://catalog.dremio.cloud/api/iceberg
ICEBERG_TOKEN=your_token
ICEBERG_WAREHOUSE=your_warehouse
ICEBERG_CATALOG_TYPE=rest
  1. Use IceFrame in your code:
from iceframe import IceFrame, col, lit, load_catalog_config_from_env
import polars as pl

# Initialize
config = load_catalog_config_from_env()
ice = IceFrame(config)

# Create an EMPTY table (note: as of 0.12, create_table never writes data
# even if you pass a DataFrame as the schema — use append_to_table afterwards)
schema = {
    "id": "long",
    "name": "string",
    "created_at": "timestamp",
}
ice.create_table("my_table", schema)

# Append data
data = pl.DataFrame({
    "id": [1, 2],
    "name": ["Alice", "Bob"],
    "created_at": [pl.datetime(2024, 1, 1), pl.datetime(2024, 1, 2)],
})
ice.append_to_table("my_table", data)

# Read data
df = ice.read_table("my_table")
print(df)

# Query Builder API — col, lit, IceFrame, QueryBuilder all importable from
# the package root as of 0.12.
from iceframe.functions import sum as ice_sum

df = (ice.query("my_table")
      .select("name", ice_sum(col("id")).alias("total_id"))
      .group_by("name")
      .execute())
print(df)

Filters: two dialects, named explicitly

filter_expr= historically accepted both an IceFrame Expression (pushed to Iceberg) and a string (Polars SQL, evaluated locally) — the same parameter name meaning two languages with opposite performance. As of 0.13.0 both are named:

# Pushed into the Iceberg scan: only matching files are read.
ice.read_table("my_table", filter=col("age") > 30)

# Evaluated locally after the scan, in Polars SQL.
ice.read_table("my_table", filter_sql="age > 30")

filter_expr= still works and dispatches on type, but the explicit parameters are preferred.

Projection and limit pushdown

# `selected_fields=("id",)` and `limit=10` are pushed into the scan, so a
# billion-row table is not materialised just to trim it.
ice.query("events").select("id").limit(10).execute()

Pushdown is skipped when it would change the answer — an ORDER BY, a join, an aggregation, or a predicate that can't be fully pushed all force the limit to be applied after the fact.

Native upsert and transactions

# Atomic, incremental MERGE — only files containing matching rows are rewritten.
ice.upsert("users", new_rows, join_cols=["id"])

# Schema change + append in a single commit.
with ice.transaction("events") as txn:
    txn.set_properties({"owner": "data-eng"})
    txn.append(arrow_table)

Metadata tables

ice.inspect("events").snapshots()   # Polars DataFrame
ice.inspect("events").files()
ice.inspect("events").partitions()

Maintenance

# Compaction scoped by a filter rewrites ONLY matching rows. Before 0.13.0 this
# replaced the entire table with the matching subset.
ice.compact_data_files("events", filter_expr="event_date >= '2026-01-01'")

# Snapshot expiry, on PyIceberg's native maintenance API. Branch/tag heads are
# always protected.
ice.expire_snapshots("events", older_than_days=30, retain_last=5)

# Orphan-file cleanup is DRY-RUN BY DEFAULT: it deletes files permanently.
candidates = ice.remove_orphan_files("events", older_than_days=7)
ice.remove_orphan_files("events", older_than_days=7, dry_run=False)

Data quality (nulls fail by default)

from iceframe.quality import DataValidator

# A null `age` FAILS "age > 0". Before 0.13.0 it passed, because Polars'
# three-valued logic filtered null rows out of the violations frame.
DataValidator().validate(df, ["age > 0"])
DataValidator().validate(df, ["age > 0"], null_policy="pass")   # opt out

# As a write gate:
ice.append_to_table("users", df, validators=["age > 0"])   # raises ValidationError

Error handling

Every deliberate IceFrame error derives from IceFrameError, so infrastructure failures are distinguishable from bad input. For backwards compatibility the input-shaped errors are still ValueError subclasses:

from iceframe import IceFrameError, CatalogError, ValidationError

try:
    ice.append_to_table("users", df, validators=["age > 0"])
except ValidationError:
    ...          # your data was bad
except CatalogError:
    ...          # the catalog was unreachable

Development

pip install -e ".[dev]"
pytest                 # runs offline against a local SQLite catalog
pytest --live          # additionally runs the live REST-catalog tests
ruff check iceframe/ tests/
ruff format --check iceframe/ tests/
mypy iceframe/

The core suite needs no credentials and no network: a session-scoped fixture builds a PyIceberg sql (SQLite) catalog with a file:// warehouse. Before 0.13.0 the read, write, query-builder, schema and stats tests only ran against the author's live Dremio catalog, so 61 of 179 tests skipped everywhere else.

Feature Comparison: IceFrame vs PyIceberg

IceFrame builds on top of PyIceberg, adding high-level abstractions and missing features.

Feature PyIceberg (Native) IceFrame (Enhanced)
Table CRUD Low-level API Simplified create_table, drop_table
Data Writing Arrow/Pandas integration Polars integration, Auto-schema inference
Branching Basic support (WIP) create_branch, fast_forward, WAP Pattern
Compaction rewrite_data_files (limited) bin_pack, sort strategies (Polars-based)
Views Catalog-dependent Unified ViewManager abstraction
Maintenance expire_snapshots GarbageCollector, Native remove_orphan_files
SQL Support None Fluent Query Builder (select, filter, join)
Ingestion add_files add_files wrapper + Incremental Ingestion recipes
Rollback manage_snapshots rollback_to_snapshot, rollback_to_timestamp
Async None AsyncIceFrame with its own bounded thread pool
Upsert / MERGE Table.upsert ice.upsert(...) wrapper + QueryBuilder.merge for column-level update rules
Transactions Table.transaction() with ice.transaction(tbl) as txn:
Metadata tables Table.inspect.* (Arrow) ice.inspect(tbl).* returning Polars frames
Data Quality None DataValidator constraints, null-failing by default, usable as a write gate
Agent surface None Read-only MCP server with row/byte caps, plus a multi-provider LLM agent

What IceFrame is not

Being straight about the limits:

  • Merge-on-read delete writes are not supported. PyIceberg 0.11 has no public delete-file writer, so MoRWriter.write_position_deletes / write_equality_deletes raise UnsupportedOperationError. Deletes are copy-on-write. Reading tables that already contain delete files works fine.
  • QueryBuilder.merge with column-level update rules reads the whole target table and overwrites it. Use ice.upsert(...) (native, incremental, atomic) whenever "replace matched rows wholesale" is what you want.
  • Joins read each joined table in full. Only the driving table gets pushdown.
  • Z-order is an approximation — a hierarchical sort, not a bit-interleaved Z-curve. The returned strategy says so.
  • Window functions run locally in Polars, after the scan.
  • Joins and column-level merge rules materialize locally. Call query.explain() before running an unfamiliar plan at scale; use native ice.upsert() or a distributed SQL engine for large mutations and joins.

Project policies

Metadata

Release files for iceframe 0.14.0

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

Source distribution (sdist)

Source distribution for iceframe 0.14.0
File Size Uploaded
iceframe-0.14.0.tar.gz 189.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for iceframe 0.14.0
File Interpreter ABI Platform
iceframe-0.14.0-py3-none-any.whl Python 3 none any Details

Total release size: 370.3 kB

Release files / iceframe-0.14.0.tar.gz

Download URL iceframe-0.14.0.tar.gz
Size 189.5 kB
Tags Source
SHA-256 checksum
How to use checksums
ee7e7a94fff83cd78cc097118f561c8ce2f2de74b7df3a4e5a8662003eeca269
BLAKE2b-256 checksum
How to use checksums
1d2cb846a08ad2825c036115ea3f5f8b4be5a274edd245af24e60214195bbc48
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.3

Release files / iceframe-0.14.0-py3-none-any.whl

Download URL iceframe-0.14.0-py3-none-any.whl
Size 180.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e582cea7a0fcd4c2f4f7e85f9a962356cac4f555a516c4f8de0b75ff6c8cd378
BLAKE2b-256 checksum
How to use checksums
7d598bc41c93c7c94ce5a54a5a1872801088117c0d68530dfcaca4762c71e7b0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.3

Release history Release notifications | RSS feed

This release

0.14.0 This release

2 release files

0.12.0

2 release files

0.11.1

2 release files

0.11.0

1 release file

0.10.0

2 release files

0.9.0

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.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