Bank statements that print dates without a year need the year taken from the statement period, not from today's date and not from the file name. U.S. Bank is the common case: a statement that runs Dec 22 2025 through Jan 21 2026 prints "Dec 29" and "Jan 6" with no year at all. ZeraBooks takes the year from the period for exactly this reason. (I founded ZeraBooks; the parser below is plain Python.)
Why is this a real bug and not a nitpick?
Because nothing else catches it. The amounts tie. The running balance ties. The file looks perfect. Then your December rows land in December 2026, eleven months in the future, and the client's year-end is wrong by whatever they spent over the holidays.
Here is the statement I'll use. Pinecrest Auto Detailing, a fictional shop, Silver Business Checking ending 2219:
Two other things to notice: withdrawals print with a trailing minus (312.48-), and card withdrawals are grouped by card with a subtotal under each group.
How do you assign the year?
Read the period first. Then for each row, try the year of the period's start; if that date falls before the start, it belongs to the next year.
import re from datetime import date, datetime from decimal import Decimal PERIOD = re.compile(r"(\w{3}) (\d{1,2}) (\d{4}) through (\w{3}) (\d{1,2}) (\d{4})") def parse_period(text): m = PERIOD.search(text) s = datetime.strptime(f"{m[1]} {m[2]} {m[3]}", "%b %d %Y").date() e = datetime.strptime(f"{m[4]} {m[5]} {m[6]}", "%b %d %Y").date() return s, e def row_date(label, start, end): """'Dec 29' or 'Jan 6' -> a date inside the statement period.""" d = datetime.strptime(f"{label} {start.year}", "%b %d %Y").date() if d < start: d = d.replace(year=start.year + 1) if not (start <= d <= end): raise ValueError(f"{label} falls outside {start} to {end}") return d def amount(raw): raw = raw.replace(",", "").replace("$", "").strip() return -Decimal(raw[:-1]) if raw.endswith("-") else Decimal(raw) start, end = parse_period("Statement Period Dec 22 2025 through Jan 21 2026") rows = [("Dec 23", "312.48-"), ("Dec 29", "1,200.00"), ("Jan 2", "286.00-"), ("Jan 6", "61.20-"), ("Jan 9", "2,764.35"), ("Jan 12", "145.90-"), ("Jan 21", "10.00-")] bal = Decimal("3845.10") for label, raw in rows: bal += amount(raw) print(row_date(label, start, end), amount(raw), bal) The last line prints 2026-01-21 -10.00 6993.87, which is the ending balance the statement prints. Dec 23 and Dec 29 come out as 2025.
The raise matters. A date outside the period means you're reading the wrong statement, or a line from the Balance Summary table, which U.S. Bank prints only "for days reflecting change". Fail loudly.
What about the subtotals?
Skip any line whose description starts with "Total" or ends with "Subtotal", then check that each group's rows add up to the subtotal you skipped. Card 0412 here: 312.48 + 61.20 = 373.68. Card 7781: 145.90. Total card withdrawals 519.58. If the group doesn't match its own subtotal, a row is missing from that group, and now you know which card to look at.
The download steps (statements from the documents area, not the CSV export, which usually has no balances) and the full worked month are on U.S. Bank statement to Excel.
Dates are the one field your balance check will never save you on.



