Skip to main content

framesmith

CI License: Apache 2.0 Python Polars Status

A preprocessing library for cleaning messy data in polars DataFrames. Composable atomic transforms, declarative recipes, expression-first design.

Real-world data arrives dirty — smart quotes, currency symbols, accounting parentheses for negatives, mainframe trailing-minus, fullwidth digits, inconsistent whitespace, and placeholder strings that quietly mean missing. framesmith cleans it, and replaces the one-off cleaning code that scatters across notebooks and silently disagrees about edge cases. It gives you small, single-purpose transforms; named recipes that bundle them into reusable pipelines as plain data; and one composition function that turns a column name and a recipe into a polars expression. Every transform returns a pl.Expr and never mutates your frame — you apply it with df.with_columns(...) or df.filter(...), so the same code runs eagerly or lazily.

Quick example

import polars as pl
import framesmith as fs

raw = pl.DataFrame({
    'customer_name': ['  ACME® Corp  ', "O'Brien & Co.", '   '],
    'amount':        ['($1,234.56)',    '$2,500-',       'N/A'],
})

cleaned = raw.with_columns(
    fs.compose_column('customer_name', fs.TEXT_NORMALIZE),
    fs.compose_column('amount',        fs.NUMERIC_STRING_TO_FLOAT),
)
print(cleaned)
# shape: (3, 2)
# ┌───────────────┬──────────┐
# │ customer_name ┆ amount   │
# │ ---           ┆ ---      │
# │ str           ┆ f64      │
# ╞═══════════════╪══════════╡
# │ ACME Corp     ┆ -1234.56 │
# │ OBrien and Co ┆ -2500.0  │
# │ null          ┆ null     │
# └───────────────┴──────────┘

Recipes are plain tuple[ExpressionTransform, ...] — splice them to extend:

from framesmith.transforms import to_snake_case

normalize_and_snake = (*fs.TEXT_NORMALIZE, to_snake_case)
df_snake = raw.with_columns(
    fs.compose_column('customer_name', normalize_and_snake)
)
# 'OBrien and Co' becomes 'obrien_and_co', etc.
# (this exact pipeline also ships ready-made as fs.TEXT_NORMALIZE_TO_SNAKE_CASE)

Installation

pip install framesmith

Or install from source:

git clone https://github.com/andrewjordan3/framesmith.git
cd framesmith
uv sync --group dev   # or: pip install -e '.[dev]'

It depends only on polars.

Key concepts

The library is organized into three tiers plus two supporting patterns.

Transforms

A transform is a pure pl.Expr → pl.Expr function. Each does exactly one thing — collapse_whitespace collapses interior whitespace runs; strip_whitespace trims the ends; normalize_unicode_nfkc applies NFKC. Transforms never call pl.col(...) themselves and never call .alias(...); the composition layer owns those boundaries, so the same transform composes into any pipeline without ceremony.

from framesmith import compose_column
from framesmith.transforms import collapse_whitespace

df.with_columns(compose_column('description', [collapse_whitespace]))

The full set of transforms lives in framesmith.transforms — see the reference for every one, grouped by domain.

Recipes

A recipe is an ordered tuple of transforms: tuple[ExpressionTransform, ...]. All recipes live in framesmith.recipes and are re-exported at the top level, so from framesmith import TEXT_NORMALIZE works directly. They follow a naming protocol so the name states what the recipe does:

  • <INPUT>_CANONICALIZE — meaning-preserving representation cleanup (whitespace, case, Unicode form).
  • <INPUT>_NORMALIZE — domain-aware cleanup that interprets the value (an address, a name, a number).
  • <INPUT>_TO_<FORM> — a conversion whose output form or dtype differs (TO_FLOAT, TO_SNAKE_CASE, TO_TITLE, …).

Because recipes are plain tuples, they compose by splicing:

my_recipe = (*fs.TEXT_NORMALIZE, to_snake_case)

And a recipe can include another recipe the same way — TEXT_NORMALIZE builds on TEXT_CANONICALIZE, which itself splices UNICODE_TO_ASCII, so the canonicalization order has exactly one source of truth.

compose_column

The single entry point that turns a column name and a recipe into an expression. Signature:

def compose_column(
    source_column_name: str,
    expression_transforms: Sequence[ExpressionTransform],
    output_column_name: str | None = None,
) -> pl.Expr: ...

It builds pl.col(source_column_name), applies each transform in order, and aliases the result back to the source column name (or to output_column_name if given). An empty transform sequence raises ValueError immediately — silent no-ops hide bugs.

Factories (configured transforms)

When configuration is genuinely data-dependent — for example, which strings count as "missing" varies by source — a transform factory takes the configuration and returns a configured ExpressionTransform. Validation and any precomputation happen once, in the factory body, so the per-call work stays cheap. Several transforms are factories — nullify_sentinels (configurable missing-value tokens), map_categories (a label remap), pad_left (fixed-width padding), and the address standardizers among them; the reference shows which transforms take configuration.

from framesmith.transforms import DEFAULT_MISSING_SENTINELS, nullify_sentinels

recipe = (*fs.TEXT_NORMALIZE, nullify_sentinels(DEFAULT_MISSING_SENTINELS))

Sentinel handling is opt-in by design and never appears in a default recipe — defaulting it on would silently null valid values (e.g. 'NA' as Namibia).

Filters (row selection)

Row selection follows the same expression-returning shape as column transforms, but the user applies the expression via df.filter(...):

from framesmith.filters import within_complete_month

monthly = df.filter(within_complete_month('transaction_date'))

Filters compose with other boolean expressions through the usual & and | — no framesmith abstraction is needed for that.

Reference

The complete reference — every transform, recipe, filter, and frame-level helper, with its signature, a short description, and an example — lives in docs/reference.md. It is organized by package: recipes first, then transforms grouped by domain (whitespace, case, unicode, numeric, names, addresses, dates, outliers, categorical, and more), then the filter, combine, group, validate, schema, and canonicalize helpers.

What's under consideration

Areas the library may grow into. None of these are commitments.

  • A polars/pandas interop layer for bridging legacy pandas pipelines.
  • Frame-level transforms beyond filters (column renaming, schema standardization, multi-column conditionals).
  • "Plans" — a layer above recipes that handles multi-column pipelines as units, so a single object can describe an entire frame's preprocessing.
  • Declarative YAML configuration for pipelines.
  • Additional filter families (null-pattern filters, numeric range filters, categorical inclusion).

Development

Engineering conventions live in CLAUDE.md. The repo uses uv for environment management.

uv run pytest                 # full suite, including src doctests
uv run ruff check src/ tests/
uv run mypy src/

The test suite covers the atomic transforms, recipes, factories, filters, the composition layer, and the regex / pattern primitives, with positive and negative cases, plus the docstring examples run as doctests.

License

Apache 2.0. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

framesmith-0.2.0.tar.gz (75.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

framesmith-0.2.0-py3-none-any.whl (79.8 kB view details)

Uploaded Python 3

File details

Details for the file framesmith-0.2.0.tar.gz.

File metadata

  • Download URL: framesmith-0.2.0.tar.gz
  • Upload date:
  • Size: 75.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","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}

File hashes

Hashes for framesmith-0.2.0.tar.gz
Algorithm Hash digest
SHA256 ee6daedf107c355d6eedf7b75c14ce2449bb2847e544eeaed52058211555d536
MD5 463c938de2d8dd5071dcb25a12d8bc18
BLAKE2b-256 f87c8fcbbca717369cadbc746371843b11d668181b7aa86072a4ff108914a0b6

See more details on using hashes here.

File details

Details for the file framesmith-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: framesmith-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 79.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","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}

File hashes

Hashes for framesmith-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 aeae0fda7d74f25bfe467cfb3e2bbdb2d022e468fd64ae6bbf9dc257a5fb1b32
MD5 582566c64e77940ef96cd512bc47a543
BLAKE2b-256 0cb2c73ea758fc72682618e3c80552b323693e13ed7523ff35408f27fd396898

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.0

2 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