banking-statements
Deterministic parsing, normalization, and validation of U.S. banking statements across institutions, account families, and historical statement formats.
banking-statements is a typed Python library for turning supported
bank statements into normalized Python domain objects while preserving
the source evidence needed to understand how each result was produced.
The library is designed around isolated statement processors. Each processor owns a known statement family and grammar, allowing support for additional banks and statement revisions to be added without destabilizing processors already proven against historical statements.
The project emphasizes strict and deterministic behavior. Unsupported statement formats, ambiguous processor matches, malformed recognized data, unknown statement behavior, and reconciliation failures during archive validation are surfaced explicitly rather than silently ignored or guessed around.
Version 0.11.0 adds a first-class high-level parsing API while retaining
the seven supported institutions introduced through 0.10.0:
Chase
Wells Fargo
American Express
Discover
Capital One
PenFed
U.S. Bank
Implemented processors cover consumer and business deposit accounts, credit cards, home-equity and business lines of credit, and personal loans. The processors are developed against private historical statement corpora while public tests remain fully synthetic and contain no private financial data.
The package intentionally focuses on answering:
What did this bank statement say?
It is not a budgeting application, bank API client, accounting system, merchant-categorization engine, personal finance manager, tax engine, database layer, or Beancount-specific importer.
- PyPI: https://pypi.org/project/banking-statements/
- Source: https://github.com/fifoa-labs/banking-statements
- License: MIT
Current Status
Current release:
banking-statements 0.11.0
Primary application API:
from banking_statements import parse_statement, parse_statements
The high-level API performs PDF reading, institution detection, deterministic processor selection, parsing, and reconciliation through one canonical package pipeline.
Supported Python versions:
Python 3.11
Python 3.12
Python 3.13
Python 3.14
Current implemented statement support:
Chase
credit card
modern statement layouts
historical statement layouts
co-branded statement layouts
observed formats spanning 2019–2026
business credit card
Ink Business Cash statements
Ink Business Unlimited statements
signed ACCOUNT ACTIVITY transaction tables
historical multi-cardholder activity
multi-page ACCOUNT ACTIVITY continuation pages
payments, refunds, purchases, fees, and interest
foreign-currency continuation detail
observed formats spanning 2019–2026
checking
Chase Total Checking statements
multi-page transaction detail
wrapped transaction descriptions
observed formats spanning 2019–2026
home-equity line of credit
advances and initial funding
payments and additional-principal payments
fee assessments and fee payments
finance-charge accrual
funds-applied and funds-reversed allocation behavior
payoff and credit-balance statements
zero-activity statements
observed formats spanning 2020–2022
Wells Fargo
checking
credit card
business checking
business credit card
business line of credit
American Express
personal credit card
business credit card
business checking
business line of credit
personal loan
observed archive coverage spanning 2013–2026
Discover
checking
Cashback Checking
Cashback Debit
legacy and current statement layouts
observed formats spanning 2018–2026
credit card
legacy transaction/post-date layouts
current single-date transaction layouts
payments and credits
purchases
fees
interest
observed formats spanning 2019–2025
Capital One
credit card
Venture X consumer credit-card statements
transaction and posting-date activity
payments and credits
purchases
fees
interest
foreign-currency detail preservation
observed formats spanning 2023–2026
business credit card
Spark business credit-card statements
Venture X Business statements
legacy flattened two-column transaction layouts
current transaction/post-date layouts
payments and credits
purchases
fees
interest where reported
observed formats spanning 2019–2026
checking
360 Checking statements
monthly and quarterly statement periods
running-balance transaction validation
wrapped transfer descriptions and reference lines
zero-activity statements
observed formats spanning 2022–2026
PenFed
home-equity line of credit
monthly HELOC statements
principal curtailment payments
payment receipt and allocation behavior
returned-payment and NSF reversals
returned-check fee assessment and reversal
finance-charge parsing and validation
legacy single-date activity tables
current process/effective-date activity tables
observed formats spanning 2024–2026
U.S. Bank
business checking
Silver Business Checking statements
deposits and withdrawals
analysis service charges
wrapped transaction and reference detail
zero-activity statements
observed formats spanning 2023–2026
credit card
Radisson Rewards Visa statements
Altitude Go Visa Signature statements
Platinum Visa statements
payments and other credits
purchases and other debits
fees and fee reversals
interest charges
other debits
multi-page transaction sections
zero-dollar fee rows
observed formats spanning 2019–2026
Current normalized domain includes:
StatementSource
SourceEvidence
StatementPeriod
StatementBalanceSummary
ParsedStatement
AccountType
AccountIdentity
TransactionEvent
TransactionDirection
Supported account types currently include:
CHECKING
SAVINGS
CREDIT_CARD
LINE_OF_CREDIT
LOAN
The complete private statement archive currently validates:
2381 / 2381 PASS
reconciliation=PASS
difference=0.00
That archive total consists of the previously proven Chase and Wells Fargo corpora plus:
Chase business credit card
89 / 89 PASS
Ink Business Cash
73 / 73 PASS
Ink Business Unlimited
16 / 16 PASS
American Express
692 / 692 PASS
Discover checking
200 / 200 PASS
Discover credit card
70 / 70 PASS
Capital One
199 / 199 PASS
consumer credit card
38 / 38 PASS
business credit card
86 / 86 PASS
checking
75 / 75 PASS
PenFed HELOC
25 / 25 PASS
U.S. Bank
105 / 105 PASS
business checking
35 / 35 PASS
credit card
70 / 70 PASS
Quality gates include Ruff, strict mypy, pytest, 100% branch coverage, distribution validation, typed-wheel validation, clean-wheel installation, and full private-archive smoke validation.
Installation
Install from PyPI:
pip install banking-statements
With uv:
uv add banking-statements
Or for development:
git clone https://github.com/fifoa-labs/banking-statements.git
cd banking-statements
uv sync --dev
Using the Package from Another Project
Application code should be institution-independent.
A consuming project should not need to know whether a PDF came from Chase,
Wells Fargo, American Express, Discover, Capital One, PenFed, or U.S. Bank.
Institution names, account-family names, processor identifiers, folder names,
and filename conventions are parser concerns owned by banking-statements.
Version 0.11.0 provides a first-class public API for the complete supported
pipeline:
from banking_statements import parse_statement
result = parse_statement("statement.pdf")
The caller supplies a PDF path. The package owns:
PDF path
↓
source hashing
↓
page-aware PDF extraction
↓
institution detection
↓
deterministic processor selection
↓
statement parsing
↓
account-type-aware reconciliation
↓
StatementParseResult
Downstream applications should not construct StatementSource,
PdfStatementTextReader, InstitutionDetector, or ProcessorRegistry for
normal parsing. Those lower-level components remain available for testing,
debugging, custom tooling, and processor development, but the supported
application entry point is the high-level API.
Parse one PDF
Import the public API directly from the package:
from banking_statements import parse_statement
result = parse_statement("statement.pdf")
parse_statement() accepts either a str or Path and returns:
StatementParseResult
statement
reconciliation
page_count
Example:
from pathlib import Path
from banking_statements import parse_statement
result = parse_statement(Path("statement.pdf"))
statement = result.statement
reconciliation = result.reconciliation
print(statement.institution)
print(statement.processor)
print(statement.account.account_type)
print(statement.account.last4)
print(statement.period.start)
print(statement.period.end)
print(statement.balances.opening_balance)
print(statement.balances.closing_balance)
print(result.page_count)
print(reconciliation.reconciled)
print(reconciliation.difference)
for transaction in statement.transactions:
print(
transaction.date,
transaction.direction,
transaction.amount,
transaction.description,
)
The calling project does not select an institution or account-family processor. Those decisions are made from evidence inside the PDF.
Parse a directory of mixed statements
Use parse_statements() for one PDF or a directory tree:
from banking_statements import parse_statements
results = parse_statements("statements")
When a directory is supplied, the package recursively discovers PDF files and processes them in deterministic path order.
For example, all of these are valid inputs:
statements/
├── chase/
│ └── checking/
│ └── 2026-07.pdf
├── usbank/
│ └── credit-card/
│ └── 2026-07.pdf
└── capitalone/
└── checking/
└── 2026-07.pdf
or the same PDFs flattened and renamed:
statements/
├── 000001.pdf
├── banana.pdf
├── document-final.pdf
├── random-name-42.pdf
└── x.pdf
The directory hierarchy and filenames are not used to determine institution or processor ownership. They are useful for human organization only.
For supported statement grammars, the same PDF should produce the same parsed result regardless of where it is stored or what the file is named.
Example:
from banking_statements import parse_statements
for result in parse_statements("statements"):
statement = result.statement
print(
statement.source.path,
statement.institution,
statement.account.account_type,
statement.account.last4,
statement.period.start,
statement.period.end,
len(statement.transactions),
result.reconciliation.reconciled,
)
The same application loop can process one institution, seven institutions, or future institutions added to the package without adding institution-specific routing to the consuming project.
StatementParseResult
The high-level API returns both the normalized statement and its reconciliation result:
result.statement
result.reconciliation
result.page_count
result.statement is the normalized ParsedStatement.
result.reconciliation is the account-type-aware
StatementReconciliation produced from that parsed statement.
result.page_count preserves the number of extracted PDF pages, which is useful
for diagnostics, audit output, and development smoke tooling.
Reconciliation remains non-destructive. A mismatch does not alter or rewrite the parsed statement.
Applications decide whether a mismatch should be accepted, warned about, or treated as an error:
from banking_statements import parse_statement
result = parse_statement("statement.pdf")
if not result.reconciliation.reconciled:
raise ValueError(
"statement did not reconcile: "
f"{result.reconciliation.difference}"
)
The private archive smoke tool uses a stricter policy and treats reconciliation mismatches as failures by default.
Deterministic failure behavior
For every PDF, the public API follows the same deterministic sequence:
read PDF
↓
detect exactly one supported institution
↓
select exactly one compatible processor
↓
parse the supported grammar
↓
reconcile the normalized statement
↓
return StatementParseResult
Failures remain explicit.
If no institution signature matches:
UnsupportedInstitutionError
If multiple institutions match:
AmbiguousInstitutionError
If the institution is recognized but no processor supports the document:
UnsupportedStatementError
If multiple processors claim the same statement:
AmbiguousProcessorError
The package does not use processor registration order, directory names, filenames, or account-number heuristics to guess around ambiguity.
Institution-independent application boundary
banking-statements is institution-aware internally and
institution-independent at the application boundary.
Internally:
banking-statements
├── institution signatures
├── Chase processors
├── Wells Fargo processors
├── American Express processors
├── Discover processors
├── Capital One processors
├── PenFed processors
└── U.S. Bank processors
Externally:
from banking_statements import parse_statement, parse_statements
A consuming application should normally care about normalized concepts such as:
ParsedStatement
AccountType
AccountIdentity
StatementPeriod
StatementBalanceSummary
TransactionEvent
TransactionDirection
StatementReconciliation
StatementParseResult
It should not need to know which institution-specific parser produced them unless that provenance is useful for diagnostics or auditing.
Adding a new supported institution or account family should expand what the same downstream integration can parse without requiring the consuming application to add a new institution-specific branch.
Folder organization is optional
The private development corpus is organized by institution and account family because that structure is useful for maintenance, historical investigation, targeted smoke runs, and human navigation.
It is not required for runtime identification.
Conceptually, these corpora are equivalent parser inputs:
organized/
├── chase/
├── wellsfargo/
├── american-express/
├── discover/
├── capitalone/
├── penfed/
└── usbank/
and:
flat/
├── 000001.pdf
├── 000002.pdf
├── 000003.pdf
└── ...
provided the PDFs themselves are unchanged and their statement grammars are supported.
The PDF is the source of truth.
Basic Usage
For normal application parsing, prefer the high-level API:
from banking_statements import parse_statement, parse_statements
The package also exposes generic domain primitives for applications that need to construct, inspect, test, or transform normalized banking objects directly.
from datetime import date
from decimal import Decimal
from pathlib import Path
from banking_statements import (
StatementPeriod,
StatementSource,
TransactionDirection,
TransactionEvent,
)
source = StatementSource(
path=Path("statement.pdf"),
sha256="example-sha256",
)
period = StatementPeriod(
start=date(2026, 7, 1),
end=date(2026, 7, 31),
)
transaction = TransactionEvent(
date=date(2026, 7, 15),
amount=Decimal("42.17"),
direction=TransactionDirection.DEBIT,
description="Sample purchase",
)
Stable processor identifiers are intentionally account-family specific. Examples include:
chase.credit_card.v1
chase.business_credit_card.v1
chase.checking.v1
chase.heloc.v1
discover.checking.v1
discover.credit_card.v1
capital_one.credit_card.v1
capital_one.business_credit_card.v1
capital_one.checking.v1
penfed.heloc.v1
us_bank.business_checking.v1
us_bank.credit_card.v1
Processor identity is part of source evidence and should remain stable for a supported grammar.
Chase Credit Card Support
Chase credit-card statements are handled through:
chase.credit_card.v1
The processor supports compatible modern, historical, and co-branded Chase credit-card statement grammars observed in the private development corpus from 2019 through 2026.
The implementation intentionally tolerates only known PDF
text-extraction artifacts demonstrated by real statements. Examples
include variants such as Opening/Closing Date versus the extracted
form O\pening/Closing Date, andNew BalanceversusN`ew Balance`.
Broad fuzzy matching and generic character de-duplication are
intentionally avoided.
Historical statements may expose full account numbers while newer
statements may expose masked numbers. AccountIdentity preserves the
display value stated by the source document and separately exposes the
last four digits when available.
Supported activity includes:
purchases
payments
merchant credits
fees
interest charges
balance transfers
My Chase Loan activity
promotional adjustments
reversals
Normalized amounts are positive magnitudes and TransactionDirection
carries the economic direction.
Chase Business Credit Card Support
Chase business credit-card statements are handled through:
chase.business_credit_card.v1
The private Chase business credit-card archive currently validates:
89 / 89 PASS
reconciliation=PASS
difference=0.00
That corpus consists of four account archives:
Ink Business Cash
73 / 73 PASS
Ink Business Unlimited
16 / 16 PASS
Observed statement formats span 2019 through 2026.
Chase business credit-card statements use a different activity grammar
from the consumer Chase credit-card processor. Rather than relying on
category sections such as PURCHASE or PAYMENTS AND OTHER CREDITS,
the supported business-card family exposes signed economic rows inside
ACCOUNT ACTIVITY.
The statement amount carries direction:
positive amount
→ DEBIT
negative amount
→ CREDIT
Normalized TransactionEvent.amount values remain positive magnitudes
while TransactionDirection carries the economic direction.
The processor supports:
purchases
payments
merchant refunds and credits
fees
interest
credit-balance refunds
foreign-currency continuation detail
historical multi-cardholder activity
multi-page ACCOUNT ACTIVITY continuation pages
zero-activity statements
cross-year transaction dates
Historical business statements can contain multiple cardholder
subsections in one account-activity table. Each cardholder subtotal is
statement bookkeeping, not an independent transaction. The processor
reconstructs the dated economic rows across those subsections without
treating cardholder labels or TRANSACTIONS THIS CYCLE totals as
activity.
Modern high-volume statements can continue activity onto later physical
pages under ACCOUNT ACTIVITY (CONTINUED). The parser preserves
transaction order across those page boundaries and keeps supported
continuation detail attached to the correct transaction.
Foreign-currency purchases can expose the original currency and exchange-rate detail on following lines. That detail is preserved as source evidence without being silently converted into a second economic transaction.
This account family has its own processor rather than broadening
chase.credit_card.v1. Consumer and business Chase credit-card matching
are kept mutually exclusive so the processor registry retains
deterministic, single-processor selection.
Chase Checking Support
Chase checking statements are handled through:
chase.checking.v1
The processor has been validated against a private chronological corpus of 182 statements spanning observed formats from 2019 through 2026:
182 / 182 PASS
reconciliation=PASS
difference=0.00
The parser supports statement identity, reporting periods, beginning and ending balances, transaction-detail tables, multi-page activity sections, wrapped transaction descriptions, deposits, withdrawals, ACH activity, card payments, transfers, fees, credits, cross-year transaction dates, debit/credit normalization, and statement reconciliation.
Checking transaction rows are reconstructed from statement text shaped like:
DATE DESCRIPTION AMOUNT BALANCE
The signed statement amount is interpreted from the checking account's perspective:
positive amount
→ CREDIT
negative amount
→ DEBIT
Normalized TransactionEvent.amount values remain positive magnitudes
while TransactionDirection carries the economic direction.
Chase checking statements can include description continuations on later physical lines. The processor reconstructs those logical rows before economic normalization rather than silently discarding continuation content.
The running-balance column remains parser evidence from the source statement; the normalized public transaction model intentionally stays focused on the transaction date, amount, direction, description, and optional source evidence.
Chase HELOC Support
Chase home-equity line-of-credit statements are handled through:
chase.heloc.v1
The processor has been validated against a private chronological corpus of 28 monthly statements spanning observed formats from 2020 through 2022:
28 / 28 PASS
reconciliation=PASS
difference=0.00
The processor normalizes debt-increasing activity such as initial funding, balance advances, assessed fees, and gross finance charges as debits. Debt-reducing activity such as additional-principal payments, fee payments, and funds actually applied to the account is normalized as credits.
Chase HELOC statements expose payment-allocation rows that are not always independent economic transactions. The processor distinguishes payment allocation detail, funds applied, and funds reversed so statement bookkeeping is not double-counted.
Finance charges are parsed from the statement's finance-charge calculation section rather than inferred from the closing summary alone. This matters when interest accrues and is paid within the same statement cycle, including payoff cycles where the closing summary may report no remaining interest even though finance charges accrued during the period.
The processor also supports zero-activity statements and credit-balance statements where the reported balance is negative.
Wells Fargo Support
Wells Fargo support covers:
checking
credit card
business checking
business credit card
business line of credit
The processors normalize supported consumer and business statements into the same domain used by every other institution.
Wells Fargo support includes account identity, statement periods, opening and closing balances, transaction reconstruction, debit/credit normalization, source evidence, and account-type-aware reconciliation.
The Wells Fargo business line-of-credit processor uses layout-aware PDF evidence where column position carries transaction direction. This is an intentional example of the package treating PDF layout as source evidence when flattened text alone is insufficient to preserve financial meaning.
American Express Support
American Express support was introduced for 0.5.0 and remains included
in 0.11.0.
Supported account families are:
personal credit card
business credit card
business checking
business line of credit
personal loan
The complete American Express private archive currently validates:
692 / 692 PASS
reconciliation=PASS
difference=0.00
The archive includes statement evidence spanning 2013 through 2026 and covers multiple historical layouts and extraction grammars.
American Express credit-card support reconstructs payments, credits, charges, fees, and interest while preserving source context such as card-ending sections and supported continuation detail.
Business checking support normalizes deposit-account activity and uses asset-account reconciliation:
opening balance
+ credits
- debits
= closing balance
Business line-of-credit and personal-loan support use debt-account reconciliation:
opening balance
+ debits
- credits
= closing balance
The personal-loan work introduced first-class LOAN account support to
the generic domain rather than encoding loan semantics as a special case
inside one processor.
Discover Checking Support
Discover checking statements are handled through:
discover.checking.v1
The processor has been validated against two private account archives totaling 200 monthly statements:
200 / 200 PASS
reconciliation=PASS
difference=0.00
Observed statement formats span 2018 through 2026.
The supported archive includes the evolution from:
CASHBACK CHECKING
to:
CASHBACK DEBIT
and the transition from legacy DiscoverBank.com statement branding to
current Discover.com branding.
The processor supports:
legacy account-ending identity
current full account-number identity
statement-period parsing
beginning and ending balances
multi-page ACCOUNT ACTIVITY sections
shared activity headers across subsections
Deposits and Credits
Checks
ATM and Debit Card Withdrawals
Electronic Withdrawals
fees and other withdrawals
service charges
zero-activity statements
Discover checking activity historically used:
Eff. Date
Bus. Date
and later:
Eff. Date
Syst. Date
The normalized transaction date uses the statement's posting/system date when available because that is the date Discover identifies as the date the transaction is generally processed and posted.
Discover Credit Card Support
Discover credit-card statements are handled through:
discover.credit_card.v1
The private Discover credit-card archive currently validates:
70 / 70 PASS
reconciliation=PASS
difference=0.00
Observed formats span 2019 through 2025.
Two major statement grammars are supported.
Legacy statements expose separate transaction and posting dates:
Trans. Date
Post Date
Newer statements expose a single transaction-date column.
For legacy statements, the normalized transaction date prefers the reported posting date. For newer statements, the reported transaction date is resolved relative to the statement closing date. This intentionally allows statement activity whose printed transaction date precedes the nominal billing-period start when the statement itself reports that activity in the current cycle.
Supported economic activity includes:
payments and credits
purchases
fees
interest
Fee and interest activity is normalized from the statement's period totals. This avoids double-counting when the same interest amount is also broken out by interest category elsewhere in the statement.
The processor also tolerates a proven PDF extraction artifact where text from an adjacent rewards column can appear after a complete transaction amount. That adjacent-column text is preserved in raw source evidence but is not silently appended to the transaction description.
Capital One Credit Card Support
Capital One consumer credit-card statements are handled through:
capital_one.credit_card.v1
The private Capital One consumer credit-card archive currently validates:
38 / 38 PASS
reconciliation=PASS
difference=0.00
Observed Venture X statement formats span 2023 through 2026.
The processor supports:
statement identity and billing periods
opening and closing balances
cardholder-scoped payments and credits
purchases
fees
interest
transaction and posting dates
foreign-currency continuation detail
cross-year transaction-date resolution
For dated activity, the normalized transaction date prefers the statement's reported posting date. This matches the account's billing-cycle behavior, including transactions whose transaction date precedes the nominal statement period but whose posting date falls inside the current cycle.
Foreign-currency detail such as the original amount, currency code, and exchange rate is preserved in raw source evidence without being silently appended to the merchant description.
Fee rows are independently checked against the statement's reported period fee total. Interest is normalized once from the statement's period total so category detail does not create duplicate economic activity.
Capital One Business Credit Card Support
Capital One business credit-card statements are handled through:
capital_one.business_credit_card.v1
The private business credit-card archive currently validates:
86 / 86 PASS
reconciliation=PASS
difference=0.00
That corpus consists of:
Spark
82 / 82 PASS
Venture X Business
4 / 4 PASS
Observed formats span 2019 through 2026.
The processor supports multiple proven historical grammars within the same Capital One business-card family.
Legacy Spark statements can flatten two visual transaction columns into one physical extracted-text line. The processor reconstructs the independent date-led transaction segments before normalization rather than treating the flattened line as one activity record.
Current Spark and Venture X Business statements use transaction/post-date activity tables. Both remain inside the same processor because the corpus demonstrates a common business credit-card family with explicit product-aware grammar boundaries.
Venture X Business statements may omit interest sections entirely. The parser does not invent missing interest behavior and instead requires only the sections proven for the matched product grammar.
Capital One Checking Support
Capital One 360 Checking statements are handled through:
capital_one.checking.v1
The private checking archive currently validates:
75 / 75 PASS
reconciliation=PASS
difference=0.00
The corpus contains two private 360 Checking accounts and includes both monthly and quarterly reporting behavior.
Supported behavior includes:
full account identity
statement-period parsing
monthly statement periods
quarterly 90/91/92-day statement periods
beginning and ending balances
deposits and credits
withdrawals and debits
transfers
wrapped transfer descriptions
reference-token continuation lines
zero-activity statements
cross-year transaction dates
running-balance validation
Capital One checking rows expose a running balance. The processor uses that reported balance as an additional parser invariant: reconstructed activity must produce the exact balance transition reported by the statement before the normalized statement reaches package-level reconciliation.
This provides two independent checks:
transaction row
→ running-balance validation
complete statement
→ asset-account reconciliation
The two observed account archives share the same 360 Checking grammar, so they are handled by one processor rather than duplicated account-specific implementations.
PenFed HELOC Support
PenFed home-equity line-of-credit statements are handled through:
penfed.heloc.v1
The private PenFed HELOC archive currently validates:
25 / 25 PASS
reconciliation=PASS
difference=0.00
Observed statement formats span 2024 through 2026.
The processor supports statement identity, reporting periods, opening and closing balances, principal curtailment payments, payment receipts, returned payments, NSF reversals, returned-check fee activity, and cycle finance charges.
PenFed HELOC activity contains multiple bookkeeping views of the same payment. A received payment can be accompanied by a separate allocation row showing how that payment was applied to interest or principal. The processor distinguishes actual economic activity from allocation detail so payments are not double-counted.
Returned-payment cycles are handled explicitly. Fee assessments, fee reversals, principal or interest reversals, replacement payments, and other recognized activity are normalized according to their effect on the debt balance rather than inferred from the summary totals.
Two proven activity-table grammars are supported. Earlier statements expose a single transaction date, while the current observed layout exposes separate process and effective dates. These layouts remain within one processor because the corpus demonstrates a stable PenFed HELOC statement family with an explicit grammar boundary.
Finance charges are parsed from the statement's finance-charge section and participate in debt-account reconciliation. Summary values remain independent validation checkpoints rather than substitutes for transaction activity.
U.S. Bank Business Checking Support
U.S. Bank business checking statements are handled through:
us_bank.business_checking.v1
The private Silver Business Checking archive currently validates:
35 / 35 PASS
reconciliation=PASS
difference=0.00
Observed statement formats span 2023 through 2026.
The processor supports statement identity and reporting periods,
beginning and ending balances, deposits, withdrawals, Other Deposits,
Other Withdrawals, analysis service charges, wrapped transaction
descriptions and reference detail, zero-activity statements, negative
balances, compact extracted dates, and cross-year transaction dates.
Economic direction follows the deposit account's perspective:
balance-increasing activity
→ CREDIT
balance-decreasing activity
→ DEBIT
Summary labels are distinguished from actual transaction-section headers so account-summary bookkeeping is not mistaken for economic activity. Analysis service charges are normalized as dated withdrawals when the statement reports them as real account activity.
U.S. Bank Credit Card Support
U.S. Bank credit-card statements are handled through:
us_bank.credit_card.v1
The private U.S. Bank credit-card archive currently validates:
70 / 70 PASS
reconciliation=PASS
difference=0.00
Observed statement formats span 2019 through 2026 and include historical Radisson Rewards Visa branding, Altitude Go Visa Signature, and Platinum Visa.
These products remain inside one processor because the private corpus demonstrates a common U.S. Bank credit-card grammar. Product branding is source evidence rather than a processor boundary.
The processor supports:
Activity Summary balance checkpoints
Payments and Other Credits
Purchases and Other Debits
Fees
fee reversals and credits
Interest Charged
Other Debits
merchant returns and statement credits
multi-page transaction continuation
zero-dollar fee rows
cross-year transaction dates
legacy and current account-number wording
Debt-increasing purchases, fees, interest, and other debits normalize as
DEBIT. Payments, refunds, statement credits, and fee reversals
normalize as CREDIT. Transaction amounts remain positive magnitudes.
The processor independently validates transaction-section totals exposed by the statement before package-level reconciliation. This prevents silent row loss or duplication from appearing successful merely because a statement was recognized.
Fees and interest are normalized only from their proven economic sections. Zero-dollar fee rows are accepted as statement bookkeeping without inventing economic activity. Repeated transaction headings on continuation pages preserve the active transaction family across page boundaries.
Historical product branding changed during the archive, but the underlying statement grammar remained compatible. Processor ownership therefore follows document structure rather than card-product names.
Statement Balances
Supported statements expose generic balance checkpoints:
StatementBalanceSummary(
opening_balance=...,
closing_balance=...,
)
These values are parsed as stated by the bank. They are not rewritten to force reconciliation.
A parser is responsible for understanding the statement grammar. Reconciliation then independently verifies whether normalized economic activity explains the reported balance movement.
Reconciliation
The high-level parsing API performs reconciliation automatically and returns it alongside the parsed statement:
from banking_statements import parse_statement
result = parse_statement("statement.pdf")
statement = result.statement
reconciliation = result.reconciliation
The lower-level reconciliation function remains available when an application
already has a ParsedStatement:
from banking_statements.reconciliation import reconcile_statement
reconciliation = reconcile_statement(statement)
Reconciliation is account-type aware.
For debt accounts such as credit cards, lines of credit, and loans:
opening balance
+ parsed debits
- parsed credits
= expected closing balance
For asset accounts such as checking and savings:
opening balance
+ parsed credits
- parsed debits
= expected closing balance
The result includes parsed debit and credit totals, expected closing
balance, difference, and a reconciled boolean. A mismatch does not
rewrite the parsed statement.
The private archive smoke tooling is stricter by default and treats a reconciliation mismatch as a smoke failure so incomplete or misdirected activity is surfaced during development.
Reconciliation is therefore not a parser shortcut. It is an independent validation layer used to prove that the extracted financial activity is economically complete.
Financial Values
Financial values use Decimal.
from decimal import Decimal
from banking_statements import to_decimal
assert to_decimal("123.45") == Decimal("123.45")
assert to_decimal("$1,234.56") == Decimal("1234.56")
assert to_decimal("(42.17)") == Decimal("-42.17")
Floating-point arithmetic is intentionally avoided for normalized financial values.
Normalized transaction amounts use positive magnitudes. Economic
direction is represented separately by TransactionDirection.
Source Evidence
Normalized statement data should remain traceable to the source statement.
from pathlib import Path
from banking_statements import SourceEvidence, StatementSource
source = StatementSource(
path=Path("statement.pdf"),
sha256="example-sha256",
)
evidence = SourceEvidence(
source=source,
page=2,
section="Account Activity",
raw_text="07/15 SAMPLE PURCHASE 42.17",
processor="example.monthly",
sequence=14,
)
Evidence can preserve information such as:
source file identity
page
section
raw text
processor
sequence
This provenance is important for auditing parser behavior, debugging future statement revisions, and verifying reconciliation decisions.
Architecture
The implemented processing pipeline is:
parse_statement(path)
↓
StatementSource + SHA-256
↓
PdfStatementTextReader
↓
page-aware, layout-aware StatementText
↓
institution detection
↓
processor selection
↓
identity and statement-balance parsing
↓
logical transaction rows
↓
focused economic normalization
↓
ParsedStatement
↓
account-type-aware reconciliation
↓
StatementParseResult
parse_statements(source) applies this same canonical pipeline to one PDF or
every recursively discovered PDF beneath a directory.
The architecture separates document mechanics from normalized financial meaning.
The domain layer should not depend on:
PDF layouts
regular expressions
specific banks
Django
databases
Beancount
application frameworks
Institution-specific extraction behavior belongs in processors. Generic financial meaning belongs in the domain.
Public API
The supported high-level application interface is:
from banking_statements import (
StatementParseResult,
parse_statement,
parse_statements,
)
Conceptually:
def parse_statement(
path: str | Path,
) -> StatementParseResult:
...
and:
def parse_statements(
source: str | Path,
) -> tuple[StatementParseResult, ...]:
...
The high-level API owns default PDF reading, institution detection, processor registry composition, processor selection, parsing, and reconciliation.
Lower-level components remain public where useful, but downstream applications should not duplicate the default orchestration recipe.
The private archive smoke runner intentionally consumes parse_statement().
This ensures the same public API used by downstream applications is continuously
exercised against the complete supported private corpus.
Processor Model
Processors represent known statement grammars.
Conceptually:
class StatementProcessor(Protocol):
@property
def name(self) -> str: ...
def match(
self,
text: StatementText,
) -> ProcessorMatch: ...
def parse(
self,
source: StatementSource,
text: StatementText,
) -> ParsedStatement: ...
Processors should be narrow enough that previously proven behavior remains stable as the package grows.
A materially different statement structure should generally receive a new processor rather than turning an existing processor into an increasingly broad universal parser.
Multiple proven historical layouts can remain inside one processor when they belong to the same statement family and can be distinguished explicitly without weakening the processor's failure behavior.
Institution Detection
Institution detection uses explicit marker signatures derived from observed statement evidence.
A signature should be specific enough to identify the institution without claiming unrelated documents from the same company.
Institution detection and processor selection are separate steps:
statement text
↓
institution signature
↓
institution
↓
processor registry
↓
exact supported account-family processor
New signatures should be added because an observed document requires them, not because a broader marker seems theoretically convenient.
Deterministic Processor Selection
ProcessorRegistry requires exactly one compatible processor.
0 matches
→ UnsupportedStatementError
1 match
→ selected
2 or more matches
→ AmbiguousProcessorError
There is intentionally no "first matching processor wins" behavior.
Processor registration order must not silently resolve ambiguous statement formats.
Development Philosophy
The central maintenance rule is:
Proven behavior stays stable.
When a future statement fails, the failure should first be classified.
New institution?
→ add institution detection and processor support
Same institution, new account family?
→ add an isolated account-family processor
Same institution and account family, materially different structure?
→ add or explicitly support a proven grammar boundary
Same processor, new economic capability?
→ add a focused capability module
Same capability, legitimate new grammar?
→ extend only that capability
Unknown or ambiguous input?
→ fail loudly
The package should grow as a library of proven document grammars rather than as one parser that attempts to understand every possible statement.
Strict Failure Policy
A parser success should mean that the known statement grammar was understood.
The package should not silently discard or guess around:
unknown transaction rows
unknown required sections
ambiguous processor matches
ambiguous amounts
unsupported date grammar
malformed recognized rows
unresolved statement identity
invalid normalized output
unexplained reconciliation differences during archive validation
Specific failures are preferred over generic parse errors because they make future statement support easier to develop and audit.
The normal development response to a new archive failure is to inspect the evidence first, not to make the parser broadly more permissive.
Logical Rows
PDF extraction often does not produce one physical line per financial transaction.
Real statements may contain:
wrapped descriptions
continuation lines
multi-line ACH details
fragmented columns
inherited dates
page breaks inside tables
adjacent-column bleed
When required, processors reconstruct logical rows before attempting to normalize economic meaning.
physical extracted lines
↓
logical statement rows
↓
economic normalization
This keeps layout reconstruction separate from transaction interpretation.
Institution Support
Current implemented support:
Chase
credit card
business credit card
checking
home-equity line of credit
Wells Fargo
checking
credit card
business checking
business credit card
business line of credit
American Express
personal credit card
business credit card
business checking
business line of credit
personal loan
Discover
checking
credit card
Capital One
credit card
business credit card
checking
PenFed
home-equity line of credit
U.S. Bank
business checking
credit card
A bank, account family, or statement format is listed as supported only after its processor has been implemented and validated against real statement evidence.
The complete private development archive currently contains 2381 supported statements, all of which pass extraction, institution detection, processor selection, parsing, normalization, and strict reconciliation with zero difference.
Private Statement Corpus
Real financial statements used during development are maintained outside the repository.
The expected local structure is:
private-data/
└── statements/
├── chase/
├── wellsfargo/
├── american-express/
├── discover/
├── capitalone/
├── penfed/
├── usbank/
└── ...
private-data/ is excluded from Git.
Real statements, account numbers, transaction histories, names, addresses, transaction references, balances, and other private financial data must never be committed to the repository or distributed in package artifacts.
Public tests use generic synthetic statement data.
Synthetic fixtures should preserve only the structural grammar required to prove parser behavior. They should never copy real names, account numbers, addresses, merchant references, transaction identifiers, balances, or other private values from the development archive.
Statement Inspection
Development includes tooling for inspecting the exact text extracted from a PDF statement.
make inspect-statement \
file="private-data/statements/example/statement.pdf"
Inspect a specific page:
make inspect-statement \
file="private-data/statements/example/statement.pdf" \
page=2
Optionally limit displayed text:
make inspect-statement \
file="private-data/statements/example/statement.pdf" \
head=3000
Parser behavior should be developed against the text actually returned by the package's PDF extraction layer rather than assumptions based only on how a PDF looks visually.
Archive Smoke Testing
Institution processors are validated against private historical statement archives.
The smoke runner calls the same public parse_statement() API intended for
downstream applications. It does not maintain a separate PDF-reading,
institution-detection, registry-selection, or parsing pipeline.
This makes full-archive smoke validation an integration test of the public API itself.
The development workflow is intentionally chronological:
01 PASS
02 PASS
03 PASS
04 FAIL
Development stops at the first failure.
That statement is inspected, the failure is classified from actual extracted evidence, and the smallest correct capability or grammar extension is added.
A generic synthetic regression test is then added before the archive resumes.
Typical usage:
make smoke-archive \
folder="private-data/statements/example"
Resume from a particular archive position:
make smoke-archive \
folder="private-data/statements/example" \
from=4
Limit the run:
make smoke-archive \
folder="private-data/statements/example" \
limit=10
Continue after failures when mapping an archive:
make smoke-archive \
folder="private-data/statements/example" \
continue=1
A strict smoke PASS means:
parse_statement() completed successfully
document extraction succeeded
institution detection succeeded
processor selection succeeded
statement identity parsed
statement period parsed
opening and closing balances parsed
logical activity reconstructed
transactions normalized
debit/credit direction assigned
statement reconciliation succeeded
Run a single statement and print normalized transactions:
uv run python -m scripts.archive_smoke \
private-data/statements/example/statement.pdf \
--show-transactions
Allow reconciliation mismatches for investigation without turning them into smoke failures:
uv run python -m scripts.archive_smoke \
private-data/statements/example \
--allow-reconciliation-failures
Include a traceback while investigating:
uv run python -m scripts.archive_smoke \
private-data/statements/example/statement.pdf \
--show-transactions \
--allow-reconciliation-failures \
--traceback
The private corpus is a local integration and regression corpus. It is never a source of public fixture data.
Development Workflow for New Statement Families
A new account family is developed from evidence rather than from assumptions.
The typical loop is:
1. Run the private archive.
2. Stop at the first failure.
3. Inspect the exact extracted statement text.
4. Compare the failure with existing processor architecture.
5. Implement the narrowest correct grammar or economic rule.
6. Add a generic synthetic regression test.
7. Run formatting, typing, and 100% branch coverage.
8. Re-run the exact failing statement.
9. Resume the archive from that position.
10. Repeat until the complete corpus reconciles exactly.
For a bounded account family, development may begin with one comprehensive investigation capture that includes the relevant processor source, synthetic tests, integration points, and extracted text for the complete private corpus. Additional investigation captures should target specific structural uncertainties exposed by the first pass rather than repeating the complete corpus.
This front-loads grammar discovery without changing the evidence standard. U.S. Bank is a concrete example: two investigation passes were enough to map two account-family processors across 105 statements before implementation, and the complete private corpus then validated 105 / 105 with zero reconciliation difference.
Private statements remain local evidence only; public repository tests continue to use generic synthetic data.
Whether development proceeds chronologically or from a bounded corpus map, changes are still made at the smallest proven grammar boundary.
The goal is not merely to make every PDF parse. The goal is to understand why each supported statement reconciles.
Development
Install development dependencies:
uv sync --dev
Format:
make format
Check formatting:
make format-check
Lint:
make lint
Type check:
make typecheck
Run tests:
make test
Run tests in parallel:
make test-fast
Run branch coverage:
make coverage
The project maintains:
100% branch coverage
Quality Gates
Run the normal validation suite:
make check
Run the CI-equivalent validation pipeline:
make ci
Before preparing a release:
make release-check
The release check validates:
formatting
linting
mypy
100% branch coverage
distribution build
distribution metadata
typed wheel contents
clean-wheel installation
A release should also be smoke-validated against the complete supported private archive before publication.
Build
Build the source distribution and wheel:
make build
Validate distributions:
make check-dist
Inspect wheel contents:
make wheel-contents
Install the built wheel into a clean environment:
make install-wheel
The distributed wheel includes:
banking_statements/py.typed
so type information is available to downstream type checkers.
Dependency Management
The project uses uv.
Synchronize the development environment:
make sync
Refresh the lockfile:
make lock
Upgrade dependencies:
make upgrade
uv.lock is committed so CI and local release validation can use
reproducible locked environments.
After changing package metadata or dependencies, refresh the lockfile before committing when required:
uv lock
Python Support
Supported Python versions:
Python 3.11
Python 3.12
Python 3.13
Python 3.14
CI validates the full supported version matrix.
Typing
banking-statements is a typed package.
The project uses strict mypy checking during development:
make typecheck
The wheel includes the PEP 561 marker:
banking_statements/py.typed
Scope
The package is intentionally narrow.
It aims to provide:
institution-independent application integration
high-level parse_statement() and parse_statements() APIs
bank statement parsing
automatic institution detection
deterministic account-family processor selection
statement normalization
source and layout evidence
strict statement validation
account-type-aware reconciliation
typed Python domain objects
It does not aim to provide:
online banking access
bank API integrations
budgeting
merchant categorization
tax accounting
bookkeeping rules
ledger rendering
Beancount-specific output
Django integration
database models
REST APIs
background jobs
web interfaces
Those concerns can consume the normalized statement objects produced by this package without becoming responsibilities of the statement parser itself.
Roadmap
Current milestone:
0.11.0
high-level public parsing API
seven-institution statement support
Chase
credit card
business credit card
checking
home-equity line of credit
Wells Fargo
checking
credit card
business checking
business credit card
business line of credit
American Express
personal credit card
business credit card
business checking
business line of credit
personal loan
Discover
checking
credit card
Capital One
credit card
business credit card
checking
PenFed
home-equity line of credit
U.S. Bank
business checking
credit card
public parse_statement() API
public parse_statements() API
StatementParseResult with reconciliation and page count
smoke runner using the same public parsing API
account-type-aware reconciliation
layout-aware PDF evidence
strict institution detection
deterministic processor selection
running-balance validation where exposed by statement evidence
100% branch coverage
2381 / 2381 private statements PASS
Next evidence-driven institution target:
To be selected from the next private statement corpus.
Expected later phases:
additional account families for supported institutions
additional institutions
additional historical statement grammars
additional reconciliation capabilities when required by statement evidence
The roadmap is evidence-driven.
Modules and abstractions should be added because real statement formats require them, not because they appear theoretically useful.
Contributing
Contributions are welcome.
Please read CONTRIBUTING.md before submitting changes.
The most important contribution rule is:
New evidence should extend the system at the smallest correct boundary without destabilizing previously proven processors.
Never include real private financial statements or personally identifiable financial information in issues, pull requests, tests, or commits.
Security
Please report security issues according to SECURITY.md.
Do not disclose private financial information or credentials in public security reports.
License
banking-statements is released under the MIT License.
See LICENSE for details.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file banking_statements-0.11.0.tar.gz.
File metadata
- Download URL: banking_statements-0.11.0.tar.gz
- Upload date:
- Size: 105.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c0c0eb930f403b460c54bbb3e287fd3cbcc75fe996192f42e9c1e0edaf421eb2
|
|
| MD5 |
d381d03361924c7eb2db4ae0af175c67
|
|
| BLAKE2b-256 |
8bec6aeb7981305f1d794ea1beaa134957015165157c6058fd28aaee508c354f
|
Provenance
The following attestation bundles were made for banking_statements-0.11.0.tar.gz:
Publisher:
publish.yml on fifoa-labs/banking-statements
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
banking_statements-0.11.0.tar.gz -
Subject digest:
c0c0eb930f403b460c54bbb3e287fd3cbcc75fe996192f42e9c1e0edaf421eb2 - Sigstore transparency entry: 2568525104
- Sigstore integration time:
-
Permalink:
fifoa-labs/banking-statements@7b7cc0e9a5f35780eff665fadba37a2b50ffb65e -
Branch / Tag:
refs/tags/v0.11.0 - Owner: https://github.com/fifoa-labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7b7cc0e9a5f35780eff665fadba37a2b50ffb65e -
Trigger Event:
release
-
Statement type:
File details
Details for the file banking_statements-0.11.0-py3-none-any.whl.
File metadata
- Download URL: banking_statements-0.11.0-py3-none-any.whl
- Upload date:
- Size: 209.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d0a30ccab45039ea5f46124ab1e7ec693485ee2ed367c8a015c899e3d54c366a
|
|
| MD5 |
8c731c497d2fd5f875c93e81a4777676
|
|
| BLAKE2b-256 |
9923f8f49d8e74f9426b0970378d956e5af79d3c09680a7a232f1e6e5587a27e
|
Provenance
The following attestation bundles were made for banking_statements-0.11.0-py3-none-any.whl:
Publisher:
publish.yml on fifoa-labs/banking-statements
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
banking_statements-0.11.0-py3-none-any.whl -
Subject digest:
d0a30ccab45039ea5f46124ab1e7ec693485ee2ed367c8a015c899e3d54c366a - Sigstore transparency entry: 2568525109
- Sigstore integration time:
-
Permalink:
fifoa-labs/banking-statements@7b7cc0e9a5f35780eff665fadba37a2b50ffb65e -
Branch / Tag:
refs/tags/v0.11.0 - Owner: https://github.com/fifoa-labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7b7cc0e9a5f35780eff665fadba37a2b50ffb65e -
Trigger Event:
release
-
Statement type: