To extract invoice data from a PDF with Python you read the text out of the file, then pull named fields out of that text. Two steps, maybe forty lines of code, and it works beautifully on the first invoice you try it on. Then supplier number seven moves their invoice number three lines up, and your parser starts returning None at two in the morning.
That second part is the real subject of this article. The reading is easy. The staying-correct is the job.
Key takeaways
- Python extracts invoice text in a few lines. Keeping it correct across hundreds of supplier layouts is what actually costs you.
- A PDF is not a data format. It is a typographic description of a printed page, which is why a regex written against one supplier's layout is a fragile thing.
- Regular expressions and per-supplier templates scale linearly with your supplier list. Vision models do not, because they read the page instead of the string.
- Whatever extracts the data, your own code has to check the arithmetic. Line items that do not sum to the subtotal are the cheapest bug detector you will ever write.
- The extraction is rarely what makes people stop building. The exception queue, the vendor matching and the accounting integration are.
The PDF format
The PDF format is versatile, enabling the accurate representation of paper documents, like invoices, without limiting their design. It comes from the world of the paper print and is designed to be a digital representation of a printed page. This flexibility offers significant freedom, allowing PDF creators to express themselves and adhere to various standards and regulations.
However, the challenge arises when data is locked within a PDF. The format's free-form and complex nature can conflict with the structured and consistent approach necessary for managing the vast data a company processes daily.

A PDF stores where each glyph sits on the page. It does not store the fact that the number in the bottom right is the total. That relationship lives in your head, and every extraction method in this article is an attempt to encode it.
What are the steps to extract data from an invoice?
An invoice is a document that usually comes in the PDF format. An invoice formalizes a transaction between a supplier and a customer, where a product or service is exchanged for a precise amount of money. Here are the steps required to extract data from this document:
- Define a schema for the data you want to extract from your invoices
- Convert your invoice from image to text
- Extract the text from your invoice according to your data schema
- Collect the extracted data

Define a schema for your invoice data
Invoices come from different suppliers and each supplier tend to customize the way their invoices look. Despite this real-life diversity in the form, the substance of all invoices is basically the same: you need a supplier, a customer, an invoice reference, a date and a list of items with an associated quantity, description and cost. A great way to start defining your invoice format would be your accounting software, as it's most probably where to store your extracted invoice data in the end, right? If you just want a data format that can cover all corner cases, let me recommend the schema.org website, which conveniently defines a series of industry-standard data formats for a lot of things, including invoices. Parseur defines a default data schema for your invoices, but you can change it to fit your use case by renaming the fields in your invoice mailbox, as explained here. Once your data format is defined, you can convert your invoice from image to text.
For example, you can define the following fields for your invoice using JSON Swagger format:
{
"InvoiceNumber": {
"type": "string",
"description": "The invoice number"
},
"InvoiceIssueDate": {
"type": "string",
"description": "The invoice date"
},
"Items": {
"type": "array",
"description": "The list of items in the invoice",
"items": {
"type": "object",
"properties": {
"quantity": {
"type": "number",
"description": "The quantity of the item"
},
"description": {
"type": "string",
"description": "The description of the item"
},
"unit_price": {
"type": "number",
"description": "The unit price of the item"
},
"price": {
"type": "number",
"description": "The total price of the item"
}
}
}
}
}
Write this down before you write any parsing code. It is the contract every method below has to satisfy, and it is the thing you will hand to a vision model later.
Convert your invoice from image to text

A PDF file can contain an image. For example, your employee may snap a quick shot of an invoice with their smartphone camera. They then save it as PDF and send it to your accounting department. Your accounting team is in charge of extracting the data from this invoice and somehow get it into your accounting system without any mistake. The next step is to convert this image to text using an Optical Character Recognition system. One of the most popular OCR system is Tesseract. Tesseract is written in C and C++. In order to use Tesseract from our Python program, we'll need to use a binding such as PyTesseract. A binding is a way to call a software library (here, Tesseract) from a language it's not written in (here, Python). There exists many such systems and their results vary wildly depending on their underlying technology and the quality of the scan of the document they are working on. Parseur transparently detects if your document is an image and automatically converts it into text, internally. Once the document's data is in text form, it is ready to get extracted.
Extract the text from your invoice according to your data schema
Once your PDF is in text (or searchable) form, you can use the pdftotext Python library to get the data out of the PDF file, as text. Here is a code snippet to extract the text from a PDF file:
import pdftotext
# Load your invoice
with open("invoice.pdf", "rb") as file_handle:
pdf = pdftotext.PDF(file_handle)
# Iterate over all the pages
for page in pdf:
print(page)
Name this script convert_pdf_to_text.py and run it, you'll get the invoice as text to the standard output.
If you want to redirect the output to a file, you can run:
$ python convert_pdf_to_text.py > invoice.txt
pdftotext gives you a flat string, which is fine for header fields and hopeless for tables. When you need the line items, reach for pdfplumber instead, because it keeps the coordinates of every word and can attempt the table itself:
import pdfplumber
with pdfplumber.open("invoice.pdf") as pdf:
page = pdf.pages[0]
# Words with their positions on the page
for word in page.extract_words():
print(word["text"], word["x0"], word["top"])
# And an attempt at the line-item table
table = page.extract_table()
if table:
for row in table:
print(row)
Run that on a real supplier invoice and you will very often find table is None. That is not a bug. It is the first honest signal that this problem is harder than it looks, and we come back to it below.
Now that you have the invoice in text form, you can extract the data you want from it, using any combination of the following techniques:
- You can use a regular expression to extract the data you want. Regular expressions are a powerful way to extract data from text, but they are also very brittle. If the invoice format changes, you'll need to update your regular expression. Also, regular expressions are not very good at extracting data from tables.
- You can use a visual templating system, ideally leveraging Dynamic OCR and Zonal OCR. This is a more advanced way to extract data from text. It's more robust than regular expressions, but it's also more complex to implement.
- You can hand the page to a vision model with a schema and let it read the document the way a person does. This is the approach that changed in the last two years, and it gets its own section below.
Let's extract data from your invoice with Python's regular expressions re module. Here is a code snippet to extract the invoice number from your invoice:
import re
# Load your invoice
with open("invoice.txt", "r") as file_handle:
invoice = file_handle.read()
# Extract the invoice number
invoice_number = re.search(r"Invoice number: (\w+)", invoice).group(1)
print(invoice_number)
Name this script extract.py and run it, you'll get the invoice number to the standard output:
$ python extract.py
And you will get something like:
INV-1234
Why your regex invoice parser breaks
A regex matches a string. An invoice is a picture. Everything that goes wrong follows from that mismatch, and it goes wrong in a small number of predictable ways:
- The label moved. Your pattern is anchored to
Invoice number:and the new template saysInvoice #, or puts the value on the next line instead of the same one. extract_tablereturnsNone. Table extractors look for ruling lines. Most supplier invoices align their columns with whitespace and draw no borders at all.- The text comes out in the wrong order. Two-column layouts and floating address blocks interleave when the page is flattened into a string, so your line-item rows arrive shuffled.
- The table spans pages. Rows two through nine are on page one, ten through fourteen on page two, with the column headers repeated in between and a subtotal line pretending to be an item.
- Three numbers all look like the total. Subtotal, total, amount due, and balance forward. Picking the largest one is wrong on any invoice carrying a credit.
- The scan is a photograph. OCR reads a smudged
8as a3and nothing downstream notices, because3is a perfectly valid digit. - Merged cells and multi-line descriptions. One product description wraps onto three lines and your row-splitting logic turns it into three items with no prices.
None of these is fixable with a better regular expression. They are all layout problems wearing a string problem's clothes. This is the point where most people either start writing one template per supplier, which grows forever, or change approach.
The 2026 approach - a vision model and a schema
The useful shift is that you no longer have to flatten the page into text before you extract from it. A vision model looks at the rendered invoice, so a supplier moving their invoice number is not an event any more. What you supply is not a pattern, it is the schema you wrote at the top of this article.
Constrain the output so you get the same keys every time. Pydantic plus a structured-output mode, such as OpenAI's structured outputs, does that for you:
from typing import List
from pydantic import BaseModel
class LineItem(BaseModel):
description: str
quantity: float
unit_price: float
amount: float
class Invoice(BaseModel):
vendor_name: str
invoice_number: str
invoice_date: str # ISO 8601
currency: str
subtotal: float
tax: float
total: float
line_items: List[LineItem]
# Render the PDF page to an image, send it to a vision model,
# and require the response to match the Invoice schema.
# The model fills the fields. It does not get to invent the shape.
That is genuinely most of the extraction problem solved, and it is why this approach spread so fast. It also introduces a new failure mode that regex never had: a regex that cannot find the invoice number returns None, while a model that cannot find it will sometimes write a plausible one. The rule that keeps you safe is simple. The model proposes and your code verifies.
If you would rather not run the model yourself, the same capability is sold as a managed service by the cloud providers, in Azure AI Document Intelligence and Amazon Textract's AnalyzeExpense, both of which return header fields and line items separately. We wrote up how the underlying approach differs from rule-based parsing in AI versus rule-based PDF parsers, and what it looks like applied to invoices specifically in vision AI invoice processing.
The validation layer you have to write yourself
Whatever produced your JSON, these checks belong in your code, not in the extractor's confidence score. They are cheap, they are deterministic, and they catch the errors that cost money:
subtotal + tax + shipping - discountlands ontotal, within a cent.- The line-item amounts sum to the subtotal. If they do not, you dropped a row or invented one.
- Every
quantity * unit_priceequals its ownamount. - The date parses, and it is not in the future.
- The invoice number has not already been paid for that vendor. Duplicate payments are the most expensive bug in accounts payable.
- The vendor name resolves to a record in your vendor master.
- The remit-to bank details match the ones already on file for that vendor. A change here is a fraud check, not a data check.
- The currency is one you actually trade in.
- The purchase order number exists, and its quantities and prices match if you run two-way or three-way matching.
- Every required field is present and non-empty.
Anything that fails goes to a person, not into the ledger:
def validate(invoice):
errors = []
if abs(invoice.subtotal + invoice.tax - invoice.total) > 0.01:
errors.append("totals_do_not_add_up")
line_sum = sum(item.amount for item in invoice.line_items)
if invoice.line_items and abs(line_sum - invoice.subtotal) > 0.01:
errors.append("line_items_do_not_sum_to_subtotal")
if not invoice.invoice_number:
errors.append("missing_invoice_number")
return errors
Ten lines of arithmetic will catch more real problems than any amount of prompt tuning.
What about invoice2data and the other libraries?
invoice2data deserves an honest mention, because it is the first result many people land on and it is a genuinely good piece of software. It is a command line tool and Python library that matches invoices against YAML templates you write per supplier, with the matching rules in version control rather than buried in your code. If you have a dozen suppliers who never change their layout, it will serve you well for years.
The limit is in the design, not the quality. One template per supplier means your maintenance grows with your supplier list, and the templates break for exactly the reasons listed above. Somewhere between twenty and thirty active layouts, the person maintaining the templates is doing more work than the person who used to retype the invoices.
The same reasoning applies to the wider library shelf. pdfplumber, PyMuPDF, pdftotext and pytesseract are all excellent at their actual jobs, which is getting characters and coordinates off a page. None of them was ever meant to know which number is the total. We compare the broader set in our roundup of the best PDF parsers and the best data extraction APIs.
Collect the extracted data
With Python, you can iterate over the invoices files in a given folder and extract the data from them. Let's say we extract the invoice number and total amount, and output the result in CSV format:
import os
import re
import pdftotext
# Iterate over all the PDF files in the folder
for filename in os.listdir("invoices/"):
if not filename.endswith(".pdf"):
continue
# Load your invoice
with open("invoices/" + filename, "rb") as file_handle:
pdf = pdftotext.PDF(file_handle)
# Print the CSV column header
print("InvoiceNumber,TotalAmount")
# Iterate over all the pages
for page in pdf:
# Extract the invoice number
invoice_number = re.search(r"Invoice number: (\w+)", page).group(1)
total_amount = re.search(r"Total amount: (\w+)", page).group(1)
print(invoice_number, total_amount, sep=",")
Name this script extract_to_csv.py and run it, you'll get the invoice number and total amount to the standard output,
that you can redirect to a CSV file that you can later open with your favorite spreadsheet software, like Excel:
$ python extract_to_csv.py > invoices.csv
A folder of CSV files is where most invoice scripts stop, and it is also where the honest accounting starts. Somebody still has to import them, match the vendors, chase the rows the accounting system rejected, and work out what to do with the invoice that failed validation at 2am. That work does not appear in your script and it does not appear in your token bill.
When to stop building
Here is the part most vendors skip, so we will say it first. If you have a handful of steady suppliers, an engineer who can watch a pipeline, and nobody waiting on the data, then write the script. A vision model plus a schema plus the ten lines of validation above will get you a long way for very little money, and you will understand every part of it.
The bill for building arrives later, and never in the extraction. It arrives in the parts nobody prototypes:
- The exception queue. Your AP team needs a screen where clicking a field highlights it on the invoice so they can fix it in four seconds instead of forty. That is a product, not a script.
- Vendor matching. "ACME Ltd", "Acme Limited" and "ACME LTD." are one supplier, and the ledger will not accept three.
- Duplicate detection. The same invoice arrives as an email attachment on Tuesday and as a statement PDF on Friday.
- The state machine. Queues, retries, partial failures, and knowing which of last night's 300 invoices actually made it through.
- The audit trail. What was extracted, what a human changed, who approved it, and when. Finance will ask, usually during an audit.
- Everything after the JSON. Field mapping into the accounting system, GL coding, purchase order matching, and the rows it rejects.
The clean signals that it is time to buy rather than build are these. You are past roughly twenty to thirty active supplier layouts. You need line items, not just header fields. More than a handful of your invoices arrive as scans. Your AP team, not your engineering team, needs to fix the mistakes. Failed extractions are delaying payments. Or, most commonly, the person maintaining the parser has stopped shipping anything else.
How Parseur handles it
Parseur is a document parser that does the whole loop, not just the extraction step. Invoices arrive at a dedicated mailbox address, through the API, or from a watched folder. The Vision AI engine reads PDFs, scans and photographs, and the Text AI engine reads emails and text documents. Fields come out named and typed, and line items come out as rows. There are no templates to write and nothing to maintain when a supplier redesigns their invoice.
What you get on top of the JSON is the half this article has been warning you about. Extracted data is reviewable and correctable in place, so an AP person fixes a misread total without opening a ticket. Corrections feed back into the extraction. And the data goes where it needs to go through direct webhook integration, Make, Zapier or Microsoft Power Automate, or straight out of the API as JSON.
If you want to see the field set we extract from invoices by default, it is documented on our invoice OCR page, and the wider workflow is covered in invoice data capture.
Conclusion
Extracting invoice data from a PDF with Python is a solved problem for about a hundred invoices and an unsolved one for ten thousand. The code is not what changes between those two numbers. What changes is how many supplier layouts you are quietly signing up to maintain, and who gets paged when one of them moves.
Write the script. It is genuinely worth doing once, if only to find out exactly which of the seven failure modes above hits you first. Then decide honestly whether the next six months of your time are best spent on the eighth. If the answer is no, Parseur has been doing this since 2016 and will take the folder off your hands.
Last updated on





