Skip to main content

messy-table

pandas.read_excel assumes your spreadsheet is well-behaved. messy-table assumes it is not.

Turn messy real-world spreadsheets — Excel/CSV exported from ERPs, legacy systems, hand-made reports — into clean, typed data, and get back an auditable report of every fix that was applied.

from messy_table import clean

result = clean("relatorio_vendas.xlsx")

result.data        # list[dict] — clean, typed rows
result.columns     # per-column name / dtype / null summary
result.report      # every transformation that was applied
result.warnings    # low-confidence decisions
result.to_pandas() # DataFrame (optional extra)

Why

AI agents and data pipelines receive arbitrary spreadsheets from users and today re-implement, by hand and fragilely, the same heuristics: find where the table starts, fix the headers, convert Excel serial dates, interpret 1.234,56. messy-table is the canonical answer to that step — deterministic, dependency-light, and fully auditable.

Install

pip install messy-table            # core — depends only on openpyxl
pip install 'messy-table[pandas]'  # adds result.to_pandas()

Python ≥ 3.10. Ships py.typed — fully type-checked.

Before & after

A typical ERP export — a title banner, a blank row, pt-BR numbers, an N/A, and a trailing totals row:

Relatório de Vendas 2024
(gerado em 01/02/2024)

Produto      | Valor (R$) | Qtd | Ativo
Café         | 1.234,56   | 10  | sim
Chá          | 2.000,00   | 5   | nao
Açúcar       | -          | 3   | sim
TOTAL        | 3.234,56   | 18  |
>>> result = clean("vendas.xlsx")
>>> result.data
[{'produto': 'Café',   'valor_r': 1234.56, 'qtd': 10, 'ativo': True},
 {'produto': 'Chá',    'valor_r': 2000.0,  'qtd': 5,  'ativo': False},
 {'produto': 'Açúcar', 'valor_r': None,    'qtd': 3,  'ativo': True}]
>>> [(c.name, c.dtype) for c in result.columns]
[('produto', 'str'), ('valor_r', 'float'), ('qtd', 'int'), ('ativo', 'bool')]

The title, blank row and TOTAL line are gone; numbers are parsed in the column's inferred locale; - is a null; sim/nao became booleans — and the report says so.

Features (v0.1)

Feature What it does
Table-start detection Skips titles, logos, stray cells and metadata before the real header.
Header detection & normalisation Finds the header (or its absence); resolves duplicate/empty/multi-row headers; slugifies to snake_case.
Merged cells Propagates a merged value across its range (fill) or keeps it top-left only (first-only).
Excel serial dates Converts 45123 → a real date when the column has a date profile (both 1900 and 1904 epochs).
Localised numbers 1.234,56 (pt-BR/EU) vs 1,234.56 (en-US), inferred per column, never per cell.
Column type inference int/float/date/datetime/bool/str, with mixed-column handling.
Disguised nulls -, N/A, n/d, #REF!, #DIV/0!, blanks → None (extensible).
Trailing junk Removes totals, signatures and footnotes after the data ends.
Cleaning report Structured, JSON-serialisable record of every correction with location and confidence.
Input formats .xlsx, .csv (delimiter + encoding sniffing), .tsv.

The report

Nothing changes without a record. Per-cell fixes are aggregated per column with a count and sample locations, so the report stays small even on huge files:

>>> print(result.report.to_json())
{
  "summary": {"table_start_detected": 1, "table_end_trimmed": 1,
              "header_renamed": 1, "null_normalized": 1,
              "number_parsed": 4, "type_coerced": 2},
  "actions": [
    {"kind": "table_start_detected", "rule": "density", "confidence": 0.8,
     "detail": "skipped 3 leading row(s) (title/metadata/blank); table starts at row 3"},
    {"kind": "number_parsed", "rule": "locale:pt_BR", "column": "valor_r",
     "count": 4, "confidence": 1.0,
     "examples": [{"row": 0, "original": "1.234,56", "final": 1234.56}]},
    ...
  ]
}

Configuration

The 80% case needs no config. For the rest:

from messy_table import clean, Config

result = clean(
    "dados.csv",
    config=Config(
        locale="pt_BR",            # force number/date interpretation; default "auto"
        header="auto",             # "auto" | int (row index) | None (no header)
        sheet=0,                   # index or name of the worksheet
        merged_cells="fill",       # "fill" | "first-only"
        null_values_extra=["s/i"], # add to the built-in null list
        strict=False,              # True: raise AmbiguityError instead of warning
    ),
)

In strict=True, a low-confidence decision raises AmbiguityError — always with a copy-pasteable Config suggestion to resolve it.

Security

messy-table parses untrusted files, so it defends against it: .xlsx archives are checked for decompression-bomb shape (absolute size and ratio) before they are opened, and a hard cell ceiling bounds memory. See ARCHITECTURE.md.

Docs

License

MIT — see LICENSE.

Metadata

Release files for messy-table 0.1.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 messy-table 0.1.0
File Size Uploaded
messy_table-0.1.0.tar.gz 45.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for messy-table 0.1.0
File Interpreter ABI Platform
messy_table-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 85.9 kB

Release files / messy_table-0.1.0.tar.gz

Download URL messy_table-0.1.0.tar.gz
Size 45.2 kB
Tags Source
SHA-256 checksum
How to use checksums
e8b2182834c5dd1578cc7ed40c577b97195037a59194dd771facafa05e74d3e6
BLAKE2b-256 checksum
How to use checksums
47ca934fef27a0ffeb3b75781b0c7c68c51ef1a436770267138dce0a2c1d855e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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 Jun 8, 2026.

Transparency log

Release files / messy_table-0.1.0-py3-none-any.whl

Download URL messy_table-0.1.0-py3-none-any.whl
Size 40.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1c6b44c1bb02a09cbc68832184ca0a1a6ca595bc073ab4cc35ee2e37a94d06c6
BLAKE2b-256 checksum
How to use checksums
a8bc5fa1a5a9e9862c2d41d2c977e1b2c119d6a6b1e1e4bb60013413c2ea8fa9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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 Jun 8, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

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