Skip to main content

statementproof

Convert bank statement PDFs to CSV — and find out when the conversion is wrong.

Most PDF-to-CSV converters hand you rows with no way to check them. The best of them (monopoly) does check totals — but tells you only that something is wrong, somewhere. The expensive failure isn't a crash you notice — it's one debit read as a credit, which reconciles to nothing and turns up weeks later inside a client's books.

A bank statement is one of the few documents that carries its own checksum: the balances. statementproof uses them.

$ statementproof october.pdf

  opening balance : 69.96
  closing balance : 586.71
  transactions    : 21 extracted, 0 skipped

  VERIFIED  (21 transactions, checks: chain, aggregate)

And when a row is misread:

  FAILED  (21 transactions, checks: chain, aggregate)
    [chain_break] row 14: running balance does not follow:
    534.66 +37.07 should give 571.73, statement shows 497.59 (off by -74.14)

See the actual output before installing → Verified, silently-failing and batch runs, copied verbatim from real runs.

Install

pip install statementproof

Python 3.9+. One dependency (pdfplumber).

Usage

# check a statement and print the report
statementproof statement.pdf

# write the transactions to CSV
statementproof statement.pdf --csv transactions.csv

# layout report you can share — contains no financial data
statementproof statement.pdf --diagnostic

# a whole folder of statements, one summary
statementproof ./january-statements/

# write a reconciliation record you can file
statementproof ./january-statements/ --record january.html

Exit code is 0 when verified, 1 otherwise, so it drops into a script. In batch mode a file that could not be opened also exits non-zero — unreadable is never counted as passing.

The reconciliation record

--record writes a document recording which exact file was checked, by whom, on what date, and what the check found. It opens in any browser and prints to PDF.

The load-bearing field is the SHA-256 of the source PDF. A record that names a file proves nothing — filenames change and statements get re-exported. The hash binds the record to one exact document, so if the PDF is later altered by a single byte, the record visibly no longer describes the file it is attached to.

In batch mode the record also names every file that could not be read, in the printed copy as well as the JSON. A record that quietly omits the statements it choked on would assert a clean review of a folder it never finished reading.

Three verdicts, and it will not bluff

verdict meaning
VERIFIED the rows reproduce the statement's opening and closing balances, and each running balance follows from the last
FAILED they don't — with the row number, expected figure, and size of the gap
UNVERIFIED the statement carries no balances to check against. Output may be perfect; nothing proves it, so nothing is claimed

A converter that returns a clean-looking CSV it cannot vouch for is the problem this exists to solve, so UNVERIFIED is never dressed up as success.

Your statement never leaves your machine

This tool asks you to point it at a document carrying your name, address, account number, employer, and every merchant you used last month. "Nothing uploaded" can't be a policy — it has to be a property of the code.

No networking library is imported anywhere in this package, and tests/test_privacy.py parses every module's AST and fails the build if one ever is. No file is written unless you name it.

The --diagnostic flag exists because a library of real statement layouts is what would most improve this tool — and a layout can be described without describing anyone's money. It reports column positions, column density, date-token shapes (DD/DD, not 10/02), and where extraction broke. It contains no amounts, balances, descriptions, dates, account numbers, names, or even the filename. It prints to your screen so you can read all of it before deciding whether to share it. The tool never sends it anywhere.

Those exclusions are asserted by tests against a known statement, not merely intended.

How it works

Geometry, not regex. The most-cited converter failure is "columns shift, debit and credit values land in the wrong places." That happens because extract_text() flattens a two-dimensional page and discards the x-position — the only signal separating a debit column from a credit column. statementproof reads each number's coordinates and clusters the money columns by their right edges.

Column roles from arithmetic, not headers. Which column holds the running balance is decided by behaviour, because header wording differs by bank and often vanishes on continuation pages.

Sign from the balance chain. If the balance went down it was a debit, whether or not a minus glyph survived extraction.

Limitations — read these first

Text-layer PDFs only. A statement downloaded from your bank's website normally has a text layer; a scan or a phone photo does not. Given one, statementproof says so rather than inventing rows. OCR is not built yet.

Arithmetic consistency, not correctness. It cannot see an error that leaves the totals intact:

  • a page header picked up as a row with a 0.00 amount
  • a wrong date or a truncated description
  • two errors that cancel out

"Provably arithmetically consistent" is the honest claim. "Provably correct" is not, and is not made here.

It has not met your bank. Seven layouts extract exactly — six written by the author, one reproduced from a real bank's published specimen. That seventh immediately found three bugs the other six could not, including MM/DD dates with no year, which took extraction from 21/21 to 0/21 while the author's own suite still reported 6/6 passing. Passing tests you wrote yourself is worth very little.

If it fails on your statement, run --diagnostic and open an issue with that output. It contains no financial data, and each new layout makes the parser better for everyone.

Tests

python statementproof/tests/make_statements.py      # build synthetic corpus
python statementproof/tests/make_real_derived.py    # build real-bank-derived layout
python statementproof/tests/score.py                # score extraction vs ground truth
python statementproof/tests/test_failure_modes.py   # validator behaviour
python statementproof/tests/test_privacy.py         # privacy promises
python statementproof/tests/test_record.py          # reconciliation record guarantees

Current: 7/7 layouts extracted exactly · 9/9 failure-mode tests · 8/8 privacy checks · 13/13 record guarantees · 0 false assurances.

"False assurance" — reporting VERIFIED on a bad extraction — is the number that matters. A validator that green-lights an error is worse than no validator.

Commercial licence

The tool is free and MIT, and every figure it produces is real whether you pay or not.

What a licence changes is one thing: it puts your firm's name on the record. Without it the record is stamped UNLICENSED — the numbers are still correct, but no one is named as having performed the review, and an unsigned document is not evidence that anyone did.

Buy a licence — $39, one-time, perpetual · unlimited statements · unlimited machines · no subscription

Then set it once per machine:

setx STATEMENTPROOF_LICENCE "Your Firm Name"        # Windows
export STATEMENTPROOF_LICENCE="Your Firm Name"      # macOS / Linux

There is no key server, no activation call, and no check-in — the package still contains no network code of any kind. The licence is an attestation, not a lock. Since the source is public, any check could be deleted in seconds; pretending otherwise would be exactly the kind of bluffing this tool exists to refuse. Editing the stamp out would produce a record naming a licence holder who did not buy one, which is forging your own audit evidence, and no payment system prevents that either.

Related

sqliteproof — the same idea for SQLite databases. PRAGMA integrity_check tells you a page number; sqliteproof tells you which tables survived, how many rows were lost, and what is safe to export. Free, MIT, no dependencies.

License

MIT.

Download files

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

Source Distribution

statementproof-0.2.2.tar.gz (27.2 kB view details)

Uploaded Source

Built Distribution

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

statementproof-0.2.2-py3-none-any.whl (29.0 kB view details)

Uploaded Python 3

File details

Details for the file statementproof-0.2.2.tar.gz.

File metadata

  • Download URL: statementproof-0.2.2.tar.gz
  • Upload date:
  • Size: 27.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for statementproof-0.2.2.tar.gz
Algorithm Hash digest
SHA256 924c310bd485ab95a3a48fddbfae60ffce5e1db1be1a5570b2754efb8e91c5ac
MD5 7822a6061359964f4d7bffb0c20b5d47
BLAKE2b-256 c1d696f91460a93df1631e0c74827ccfc954f01b2690058498e3ba520cec00b0

See more details on using hashes here.

File details

Details for the file statementproof-0.2.2-py3-none-any.whl.

File metadata

  • Download URL: statementproof-0.2.2-py3-none-any.whl
  • Upload date:
  • Size: 29.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for statementproof-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 58e4866a1155eb1ec1a744fb5607d6fe3605d1d39f359a90e16b0168985a2b10
MD5 70210dc42b6a50cdc0b41180c25a8d09
BLAKE2b-256 ba7aca2a4f207830ab59e2d29764e98fa73f3b2b78d89bd0db8ea50406bf593b

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.2 This release

2 files

0.2.1

2 files

0.2.0

2 files

0.1.1

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