Hardware FixRecommendedDevice not working? Your driver may be the problemCheck updates for common hardware issues.Fix DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run Scan×
Skip to content
Automation

How to Generate PDF Invoices with Python: ReportLab and WeasyPrint

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use ReportLab when you want to place invoice elements directly on a PDF canvas. Use WeasyPrint when you want to write an HTML/CSS invoice template and render it to PDF. In both cases, keep invoice data, calculations, and rendering separate. That design makes totals testable, templates replaceable, and output easier to audit.

This guide gives complete Python examples for both routes, explains their trade-offs, and covers layout, pagination, security, installation, troubleshooting, and the legal limits of a PDF library. The required invoice fields and tax rules depend on your jurisdiction and transaction type, so verify them with the relevant tax authority before using an invoice operationally.

Choose the rendering approach first

Decision ReportLab WeasyPrint
Layout authoring Python drawing calls on a PDF canvas HTML and CSS rendered through the Python API
Coordinate model Direct placement; the default origin is the lower-left corner Document flow, print CSS, tables and page rules
Best fit A programmatic layout with precise placement A web-oriented template that designers can edit
Main caution You must manage positioning and page breaks yourself Untrusted HTML or CSS can create security problems

These are workflow differences documented by the projects, not performance benchmarks. ReportLab describes itself as a Python library for directly creating PDF documents; its introduction and canvas guide document the drawing model. WeasyPrint documents HTML-to-PDF rendering and invoice use cases in its common use cases and first steps guides.

Model invoice data before drawing anything

Do not calculate totals inside PDF drawing code. Store a structured invoice, calculate each line and the grand total once, then pass the resulting values to a renderer.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from decimal import Decimal, ROUND_HALF_UP
from dataclasses import dataclass

@dataclass
class LineItem:
    description: str
    quantity: Decimal
    unit_price: Decimal
    tax_rate: Decimal


def money(value: Decimal) -> Decimal:
    return value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)


def calculate_totals(items):
    rows = []
    subtotal = Decimal("0")
    tax_total = Decimal("0")
    for item in items:
        net = money(item.quantity * item.unit_price)
        tax = money(net * item.tax_rate / Decimal("100"))
        rows.append({"description": item.description, "quantity": item.quantity,
                     "unit_price": money(item.unit_price), "net": net,
                     "tax": tax, "tax_rate": item.tax_rate})
        subtotal += net
        tax_total += tax
    return rows, money(subtotal), money(tax_total), money(subtotal + tax_total)

invoice = {
    "number": "INV-001",
    "issue_date": "2026-09-29",
    "currency": "USD",
    "seller": {"name": "Example Studio", "address": "1 Main Street, London"},
    "customer": {"name": "Acme Ltd", "address": "20 Market Road, London"},
    "items": [
        LineItem("Consulting", Decimal("4"), Decimal("125.00"), Decimal("20")),
        LineItem("Support", Decimal("1"), Decimal("50.00"), Decimal("20")),
    ],
}
rows, subtotal, tax_total, total = calculate_totals(invoice["items"])

Using Decimal avoids binary floating-point surprises. Decide and document whether tax is rounded per line or only after summing; the correct convention can be jurisdiction-specific. Also decide how to represent credits, discounts, zero-rated lines, exempt lines, payment terms, bank details, and invoice numbering before styling the PDF.

Route A: generate an invoice with ReportLab

Install the library

The documented open-source installation starts with:

python -m pip install reportlab

ReportLab’s installation guidance notes that optional extras may be needed for particular graphics features. Confirm the requirements for the version and features you deploy in the installation guide.

Complete drawing example

The canvas uses points and starts at the lower-left origin by default. This example creates a letter-sized invoice, wraps long descriptions, draws a table, and starts a second page when the line-item area is full.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from reportlab.lib import colors
from reportlab.lib.pagesizes import letter
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
from reportlab.pdfbase.pdfmetrics import stringWidth


def draw_wrapped(c, text, x, y, max_width, leading=12, font="Helvetica", size=9):
    c.setFont(font, size)
    words = text.split()
    line = ""
    lines = []
    for word in words:
        trial = (line + " " + word).strip()
        if stringWidth(trial, font, size) <= max_width:
            line = trial
        else:
            if line:
                lines.append(line)
            line = word
    if line:
        lines.append(line)
    for value in lines:
        c.drawString(x, y, value)
        y -= leading
    return y


def header(c, inv, width, height):
    c.setFont("Helvetica-Bold", 20)
    c.drawString(20 * mm, height - 25 * mm, "INVOICE")
    c.setFont("Helvetica", 9)
    c.drawRightString(width - 20 * mm, height - 22 * mm, f"Number: {inv['number']}")
    c.drawRightString(width - 20 * mm, height - 29 * mm, f"Issued: {inv['issue_date']}")
    y = height - 50 * mm
    c.setFont("Helvetica-Bold", 10)
    c.drawString(20 * mm, y, "From")
    c.drawString(110 * mm, y, "Bill to")
    c.setFont("Helvetica", 9)
    draw_wrapped(c, f"{inv['seller']['name']}n{inv['seller']['address']}",
                 20 * mm, y - 14, 75 * mm)
    draw_wrapped(c, f"{inv['customer']['name']}n{inv['customer']['address']}",
                 110 * mm, y - 14, 75 * mm)


def make_pdf(inv, rows, subtotal, tax_total, total, filename="invoice.pdf"):
    c = canvas.Canvas(filename, pagesize=letter)
    width, height = letter
    y = height - 95 * mm
    header(c, inv, width, height)

    def table_header():
        nonlocal y
        c.setFillColor(colors.HexColor("#eeeeee"))
        c.rect(20 * mm, y - 5, width - 40 * mm, 18, fill=1, stroke=0)
        c.setFillColor(colors.black)
        c.setFont("Helvetica-Bold", 8)
        c.drawString(22 * mm, y, "Description")
        c.drawRightString(130 * mm, y, "Qty")
        c.drawRightString(155 * mm, y, "Unit")
        c.drawRightString(190 * mm, y, "Net")
        y -= 22

    table_header()
    for row in rows:
        if y < 45 * mm:
            c.showPage()
            y = height - 25 * mm
            c.setFont("Helvetica-Bold", 12)
            c.drawString(20 * mm, y, f"Invoice {inv['number']} (continued)")
            y -= 18
            table_header()
        c.setFont("Helvetica", 8)
        y = draw_wrapped(c, row["description"], 22 * mm, y, 95 * mm, leading=10, size=8)
        c.drawRightString(130 * mm, y + 10, str(row["quantity"]))
        c.drawRightString(155 * mm, y + 10, f"{row['unit_price']:.2f}")
        c.drawRightString(190 * mm, y + 10, f"{row['net']:.2f}")
        y -= 8

    y -= 8
    c.line(115 * mm, y, 190 * mm, y)
    c.setFont("Helvetica", 9)
    for label, value in (("Subtotal", subtotal), ("Tax", tax_total), ("Total", total)):
        y -= 15
        c.drawString(120 * mm, y, label)
        c.drawRightString(190 * mm, y, f"{inv['currency']} {value:.2f}")
    c.setFont("Helvetica", 8)
    c.drawString(20 * mm, 20 * mm, "Payment terms and legally required notices go here.")
    c.save()

make_pdf(invoice, rows, subtotal, tax_total, total)

The short skeleton in the official canvas guide follows the same lifecycle: create a canvas, draw text, call showPage(), then call save(). Choose the page size deliberately for the destination rather than assuming the default is appropriate.

ReportLab failure modes

  • Text runs off the page: measure or wrap strings, reduce font size, and reserve a footer area.
  • Rows overlap: decrement the vertical cursor after every wrapped line, not just every item.
  • Missing characters: register and embed a font that contains the required glyphs; test currency symbols and non-Latin customer names.
  • Unreadable multipage output: repeat the table header after showPage() and include the invoice number on continuation pages.

Route B: render an HTML invoice with WeasyPrint

Install and understand the dependency boundary

Install the Python package according to the platform instructions in WeasyPrint’s documentation. A minimal API call is:

from weasyprint import HTML

html = """
<h1>Invoice</h1>
<p>Invoice number: INV-001</p>
"""
HTML(string=html).write_pdf("invoice.pdf")

For a production invoice, generate escaped values from structured data and keep print rules in CSS. Never concatenate arbitrary customer-provided HTML into the template.

Complete HTML/CSS rendering example

from html import escape
from weasyprint import HTML


def invoice_html(inv, rows, subtotal, tax_total, total):
    item_rows = "".join(
        f"<tr><td>{escape(str(r['description']))}</td>"
        f"<td class='num'>{r['quantity']}</td>"
        f"<td class='num'>{r['unit_price']:.2f}</td>"
        f"<td class='num'>{r['net']:.2f}</td></tr>"
        for r in rows
    )
    return f"""<!doctype html>
<html><head><meta charset='utf-8'>
<style>
@page {{ size: A4; margin: 18mm 16mm 20mm; @bottom-right {{ content: 'Page ' counter(page) ' of ' counter(pages); font-size: 8pt; }} }}
body {{ font-family: sans-serif; color: #222; font-size: 10pt; }}
h1 {{ margin: 0; }}
.meta {{ float: right; text-align: right; }}
.addresses {{ display: flex; justify-content: space-between; margin: 18mm 0 12mm; }}
table {{ width: 100%; border-collapse: collapse; page-break-inside: auto; }}
thead {{ display: table-header-group; }}
tr {{ page-break-inside: avoid; }}
th, td {{ border-bottom: 1px solid #ddd; padding: 7px 4px; text-align: left; }}
.num {{ text-align: right; }}
.totals {{ margin-left: auto; width: 45%; }}
.totals td {{ border: 0; }}
</style></head><body>
<header><h1>Invoice</h1>
<div class='meta'>Number: {escape(inv['number'])}<br>Issued: {escape(inv['issue_date'])}</div></header>
<section class='addresses'><div><strong>From</strong><br>{escape(inv['seller']['name'])}<br>{escape(inv['seller']['address'])}</div>
<div><strong>Bill to</strong><br>{escape(inv['customer']['name'])}<br>{escape(inv['customer']['address'])}</div></section>
<table><thead><tr><th>Description</th><th class='num'>Qty</th><th class='num'>Unit</th><th class='num'>Net</th></tr></thead>
<tbody>{item_rows}</tbody></table>
<table class='totals'><tr><td>Subtotal</td><td class='num'>{inv['currency']} {subtotal:.2f}</td></tr>
<tr><td>Tax</td><td class='num'>{inv['currency']} {tax_total:.2f}</td></tr>
<tr><th>Total</th><th class='num'>{inv['currency']} {total:.2f}</th></tr></table>
</body></html>"""

HTML(string=invoice_html(invoice, rows, subtotal, tax_total, total), base_url=".").write_pdf("invoice.pdf")

CSS gives you familiar tools such as table headers that repeat on new pages, controlled margins, and page-break-inside: avoid. The WeasyPrint invoice use-case documentation also shows workflows involving PDF/A and a Factur-X XML attachment; adapt those file contents to your actual invoice rather than copying the example unchanged.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

WeasyPrint security boundary

WeasyPrint warns that untrusted HTML or CSS can create security problems. Use a fixed, reviewed template; escape values; restrict accessible resources; and isolate rendering if users can influence invoice content. Do not accept arbitrary CSS, file URLs, scripts, or network references from an untrusted tenant.

Validate the generated PDF before sending it

  1. Recalculate the totals independently of the renderer and compare the displayed subtotal, tax, and total.
  2. Open PDFs produced with short and very long descriptions, many rows, zero quantities where allowed, discounts, non-ASCII names, and multiple tax rates.
  3. Check page boundaries, repeated headers, footer text, date and currency formatting, and that no address or total is clipped.
  4. Extract text in an automated test or inspect the PDF manually; also print or view it at normal zoom.
  5. Store the source data and a stable invoice number so a regenerated file can be compared with the issued record.

Legal and operational limits

ReportLab and WeasyPrint only create the document. They do not decide which fields are legally required, how tax must be calculated, whether electronic invoicing is mandatory, which language or currency rules apply, or how long records must be retained. Those requirements vary by jurisdiction, transaction type, and business status. Obtain the applicable rules from your tax authority or regulator and have a qualified adviser review your process.

Or skip the browser setup

If your invoice already exists as a web page and you need a visual check or a shareable capture, ScreenshotNeo can take the screenshot without you managing a headless browser. Its API accepts a URL and can return PNG, JPEG, WebP, or PDF.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for all parameters. The equivalent Python and Node.js calls are:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', data));

Before capture, ScreenshotNeo can accept consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status with X-Page-Verdict and X-Billed headers. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Sign up for ScreenshotNeo to start with the free allowance.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting checklist

“No module named reportlab” or “No module named weasyprint”

Install into the same virtual environment used to run the script: python -m pip install reportlab or the platform-specific WeasyPrint installation steps. Confirm the interpreter with python -c "import sys; print(sys.executable)".

The PDF is blank

For ReportLab, verify that drawing calls occur before save(). For WeasyPrint, inspect the generated HTML string, ensure the data is not empty, and pass a correct base_url when loading local assets.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Images or fonts do not appear

Use resolvable paths or URLs, set base_url for WeasyPrint, and register fonts explicitly in ReportLab. Test in the same restricted environment used in production.

Rows split badly across pages

In ReportLab, reserve space and start a new page before drawing a row that will not fit. In WeasyPrint, use table headers, page-break-inside: avoid, and conservative margins; very large unbreakable content may still require a deliberate page break.

FAQ

Frequently Asked Questions

Can I generate an invoice entirely in memory?

Yes. Both libraries can write to a file-like object such as an io.BytesIO buffer; return those bytes from a web endpoint instead of writing a temporary file.

Should I use PDF/A for every invoice?

Not automatically. PDF/A, Factur-X, and attached XML can be appropriate in workflows that require archival or structured electronic invoices, but the applicable requirement comes from your jurisdiction and customer agreement.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How do I prevent duplicate invoice numbers after a retry?

Allocate the invoice number in a durable database transaction, enforce a uniqueness constraint, and make PDF generation idempotent for that number. Rendering should not be the component that assigns numbering.

Can a PDF prove that tax was calculated correctly?

No. It displays the values your application supplied. Keep the input data, rounding policy, tax-rate source, and calculation tests so the result can be audited.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Read next

Recommended PC Tool
Recommended PC Tool
Outdated Drivers Are Slowing You DownFree scan - exact matches
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.