anofox-tabular Python package
Python wrapper for the anofox-tabular DuckDB extension — data quality, PII detection, email/phone validation, anomaly detection, diffing, money, and VAT primitives.
Installation
pip install anofox-tabular
Optional extras for DataFrame support:
pip install "anofox-tabular[pandas]" # adds pandas
pip install "anofox-tabular[polars]" # adds polars
pip install "anofox-tabular[pandas,polars]"
Quick start
import anofox
# In-memory database (extension downloaded automatically)
with anofox.connect() as conn:
# Email validation
print(conn.execute("SELECT anofox_tab_email_is_valid('hi@example.com', 'regex')").fetchone())
# Or use a locally built extension
conn = anofox.connect(
extension_path="/path/to/anofox_tabular.duckdb_extension"
)
Python-native API
import anofox
from anofox import validate, quality, pii, diff
conn = anofox.connect()
# ── Email validation ──────────────────────────────────────────────────
validate.email_is_valid(conn, "hi@example.com") # True
validate.email_is_valid(conn, "hi@example.com", mode="dns") # True (DNS checked)
import pandas as pd
df = pd.DataFrame({"email": ["a@b.com", "bad-email", "c@d.org"]})
result_df = validate.email_is_valid(conn, df, column="email")
# Returns DataFrame with added 'email_is_valid' column
# ── Phone validation ──────────────────────────────────────────────────
validate.phone_is_valid(conn, "+14155552671", region="US") # True
validate.phone_format(conn, "+14155552671", "US", "INTERNATIONAL")
# ── Data quality ──────────────────────────────────────────────────────
conn.execute("CREATE TABLE orders AS SELECT * FROM read_parquet('orders.parquet')")
quality.volume(conn, "orders", min_rows=100)
# {"status": "pass", "min_rows": 100, ...}
quality.null_rate(conn, "orders", "amount", max_null_rate=0.05)
quality.distinct_count(conn, "orders", "status", min_distinct=2, max_distinct=10)
quality.schema_check(conn, "orders", ["id", "amount", "created_at"])
# ── High-level profile ────────────────────────────────────────────────
summary = conn.profile(df) # returns pd.DataFrame with per-column metrics
# ── PII detection ─────────────────────────────────────────────────────
pii.pii_contains(conn, "Call me at +1-415-555-2671") # True
pii.pii_detect(conn, "Email: test@example.com") # [{"type": "EMAIL", ...}]
pii.pii_mask(conn, "test@example.com", strategy="redact")
scan_result = pii.pii_scan_table(conn, "orders") # pd.DataFrame
# ── Diff ──────────────────────────────────────────────────────────────
# Table names or DataFrames both work
changes = diff.joindiff(conn, "orders_v1", "orders_v2", primary_keys="id")
changes = diff.joindiff(conn, df_before, df_after, primary_keys="id")
# Returns pd.DataFrame with diff_type: 'added', 'removed', 'changed', 'unchanged'
# ── Schema validation ─────────────────────────────────────────────────
from anofox.validate import EmailRule, PhoneRule
result = conn.validate(df, schema={
"email": EmailRule(mode="dns"),
"phone": PhoneRule(region="DE"),
})
print(result.passed) # True / False
print(result.failures) # pd.DataFrame of failed rows
Module overview
| Module | Functions |
|---|---|
anofox.validate |
email_is_valid, email_validate, phone_is_valid, phone_parse, phone_format, phone_region |
anofox.quality |
volume, null_rate, distinct_count, freshness, zscore, iqr, schema_check |
anofox.anomaly |
isolation_forest, isolation_forest_mv, dbscan, dbscan_mv, outlier_tree |
anofox.pii |
pii_detect, pii_mask, pii_contains, pii_scan_table, pii_audit_table |
anofox.diff |
joindiff, hashdiff |
anofox.money |
make_money, money_from_cents, is_valid_currency, currency_symbol, money_add, etc. |
anofox.vat |
make_vat, vat_is_valid, vat_is_eu_member, vat_country_name, etc. |
CLI
# Profile any file (colored table output)
anofox profile data.parquet
anofox profile data.csv --format json
# Quality checks (exit 0 = pass, exit 1 = fail)
anofox quality data.parquet --volume-min 1000
anofox quality data.csv --null-max 0.05 --column email
Supported formats: .parquet, .csv, .tsv, .json, .ndjson
pytest plugin
# Run with: pytest --anofox-check
import pytest
@pytest.mark.anofox_quality("orders", volume_min=100)
def test_orders_table_has_data(anofox_conn):
...
The anofox_conn session-scoped fixture is provided automatically. Tests skip if the extension is unavailable.
Extension resolution
The package resolves the extension binary in this order:
extension_pathargument toconnect()(path to a local binary)ANOFOX_EXT_PATHenvironment variable- Cached binary in
~/.anofox/extensions/<duckdb version>/<platform>/ - Download from the DuckDB community registry, then the S3 mirror
(
https://get.erpl.io), both laid out as<base>/v<duckdb version>/<platform>/anofox_tabular.duckdb_extension.gz
Downloads are verified against a .sha256 sidecar published next to the
archive. The S3 mirror publishes sidecars for every build; the community
registry does not, so by default downloads come from the mirror. Set
ANOFOX_ALLOW_INSECURE_DOWNLOAD=1 to accept archives without a sidecar.
Development
# Build the extension first
make release
# Install package in dev mode
cd python
pip install -e ".[dev]"
# Run tests
ANOFOX_EXT_PATH=../build/release/extension/anofox_tabular/anofox_tabular.duckdb_extension \
pytest tests/ -v
# Loader/utils tests run without extension (no env var needed)
pytest tests/test_loader.py tests/test_utils.py -v
License
MIT
Metadata
Release files for anofox-tabular 2026.10.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| anofox_tabular-2026.10.4.tar.gz | 83.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| anofox_tabular-2026.10.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 116.4 kB
Release files / anofox_tabular-2026.10.4.tar.gz
| Download URL | anofox_tabular-2026.10.4.tar.gz |
|---|---|
| Size | 83.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8e272c801212ea92bd5c965c3c744723c27aff60258f2533c05f287b716d2010
|
|
BLAKE2b-256 checksum How to use checksums |
902cc66ce4944f6ffcb11da1f6f449bb380c213dac41f72107c9139835882fa7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 4, 2026.
Transparency logRelease files / anofox_tabular-2026.10.4-py3-none-any.whl
| Download URL | anofox_tabular-2026.10.4-py3-none-any.whl |
|---|---|
| Size | 33.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7657488edebe772770f8bd503984dd0bac089ed8bf14add02fe359589732753d
|
|
BLAKE2b-256 checksum How to use checksums |
d6a44e5a3862db7cdefcf9e69d1472f1869debffe33e7d10cbb8130ec3062d39
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 4, 2026.
Transparency log