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

Generate reliable data-driven PDFs by separating three jobs: normalize and validate the data, describe the document layout, and render and inspect the final file. In Python, use ReportLab when a Python-native drawing or flowable layout is the best fit; use WeasyPrint when your report is naturally HTML and CSS. Both approaches can produce production documents, but neither removes the need to test real data, page breaks and renderer limitations.

Start with a data-to-document pipeline

A maintainable PDF generator should not format raw database rows directly inside drawing calls or HTML templates. Use a pipeline with explicit boundaries:

  1. Extract: read records from the database, API, CSV or another source.
  2. Normalize: convert dates, numbers, currencies, names and status values into consistent internal types.
  3. Validate: reject or quarantine missing identifiers, invalid dates, impossible totals and unsupported values.
  4. Present: create display-ready values such as localized dates, thousands separators and controlled labels.
  5. Render: pass the presentation model to ReportLab or an HTML/CSS template.
  6. Verify: inspect the generated PDF for wrapping, pagination, links, page size and required features.

Keeping transformation separate from presentation lets you change typography or page layout without changing business calculations. It also makes the same validated data usable for CSV exports, web views and PDFs.

A small normalized model

from dataclasses import dataclass
from datetime import date
from decimal import Decimal

@dataclass
class InvoiceLine:
    description: str
    quantity: int
    unit_price: Decimal

    @property
    def total(self) -> Decimal:
        return self.quantity * self.unit_price


def display_line(line: InvoiceLine) -> dict:
    return {
        "description": line.description or "(unnamed item)",
        "quantity": f"{line.quantity:,}",
        "unit_price": f"${line.unit_price:,.2f}",
        "total": f"${line.total:,.2f}",
    }

Decide before rendering how an empty value appears, how long labels wrap, which timezone applies to timestamps, and which locale controls decimal and date formatting. Do not silently turn invalid input into zero: make that policy explicit and log rejected records.

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

Choose ReportLab or WeasyPrint

Criterion ReportLab WeasyPrint
Authoring model Python drawing APIs and higher-level document layout objects HTML structure styled with CSS
Good fit Programmatic drawing, tightly controlled coordinates, Python-native reports and flowables Reports already designed as markup, CSS-based typography and print styles
Tables Table objects can calculate row heights, split across pages and repeat header rows HTML tables and print CSS; verify behavior with your actual template
Output assumptions Choose page size explicitly; canvas measurements use points Write a PDF file or obtain PDF bytes from HTML
Compatibility caveat Validate your chosen flowables and drawing code Unsupported CSS properties can produce warnings; check documented feature support

There is no documented speed or fidelity winner. Render representative short and long datasets with the exact fonts, links, charts and page features your users need.

Generate a structured PDF with ReportLab

ReportLab includes a low-level pdfgen canvas for painting text and graphics and higher-level layout constructs for reports. The following example uses a Platypus table so long rows can flow to another page and the header can repeat.

from decimal import Decimal
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle

rows = [
    {"description": "Consulting", "quantity": 3, "unit_price": Decimal("125.00")},
    {"description": "Implementation and documentation", "quantity": 8, "unit_price": Decimal("95.00")},
]

out = "invoice.pdf"
doc = SimpleDocTemplate(
    out,
    pagesize=A4,
    rightMargin=18 * mm,
    leftMargin=18 * mm,
    topMargin=16 * mm,
    bottomMargin=16 * mm,
)
styles = getSampleStyleSheet()
body = styles["BodyText"]
heading = styles["Heading1"]

data = [["Description", "Qty", "Unit price", "Total"]]
for item in rows:
    total = item["quantity"] * item["unit_price"]
    data.append([
        Paragraph(item["description"], body),
        f"{item['quantity']:,}",
        f"${item['unit_price']:,.2f}",
        f"${total:,.2f}",
    ])

table = Table(data, colWidths=[95 * mm, 18 * mm, 30 * mm, 30 * mm], repeatRows=1)
table.setStyle(TableStyle([
    ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#243447")),
    ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
    ("GRID", (0, 0), (-1, -1), 0.25, colors.HexColor("#BBBBBB")),
    ("ALIGN", (1, 1), (-1, -1), "RIGHT"),
    ("VALIGN", (0, 0), (-1, -1), "TOP"),
    ("BOTTOMPADDING", (0, 0), (-1, 0), 7),
    ("TOPPADDING", (0, 0), (-1, 0), 7),
]))

story = [Paragraph("Invoice", heading), Spacer(1, 8), table]
doc.build(story)

repeatRows=1 repeats the heading when the table crosses a page break. Explicit column widths prevent a long description from stealing all available space. Use Paragraph cells rather than bare strings where text must wrap. For coordinate-level work, create a canvas with an explicit page size:

from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter

c = canvas.Canvas("drawing.pdf", pagesize=letter)
c.setFont("Helvetica", 11)
c.drawString(72, 720, "A precisely positioned label")
c.showPage()
c.save()

Canvas coordinates and page sizes are measured in points. Set the size deliberately instead of depending on a default, especially when the output is printed or combined with other documents.

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

Render an HTML/CSS report with WeasyPrint

WeasyPrint accepts HTML and CSS and can write a PDF to a path or return PDF bytes. This route is convenient when designers already work in templates and print styles.

from weasyprint import HTML

html = """



  
  


  

Invoice

DescriptionQtyUnit priceTotal
Consulting3$125.00$375.00
""" HTML(string=html, base_url=".").write_pdf("invoice.html.pdf") # To obtain bytes instead: pdf_bytes = HTML(string=html, base_url=".").write_pdf()

Set base_url when the document references local stylesheets, images or fonts. WeasyPrint warns when CSS is unsupported; treat warnings as test failures for features that affect readability. Verify running headers, page breaks, external links, images and fonts with representative data rather than assuming browser CSS support transfers unchanged.

Tables, pagination and difficult values

Long tables

  • Define widths or a deliberate responsive strategy; uncontrolled content can cause clipping or unreadably narrow columns.
  • Repeat column headings on every page. ReportLab supports this directly with repeatRows; HTML templates should use a table header group and be tested with your renderer.
  • Render a dataset large enough to cross several pages. A one-page sample cannot reveal orphaned headings, split totals or rows that do not fit.

Text and numbers

  • Wrap or shorten labels according to a documented rule; never truncate silently when the value is legally or financially significant.
  • Use decimal arithmetic for currency, then format only at the presentation boundary.
  • Keep source dates timezone-aware and convert them to the report’s declared timezone before formatting.
  • Represent missing values consistently, such as “Not provided,” and distinguish them from zero.

Special PDF requirements

Identify required links, bookmarks, forms, attachments, accessibility behavior and page ranges before choosing the renderer. Test each feature in the final PDF viewer, not only in source HTML or Python objects.

A verification checklist for every build

  • Open the PDF and confirm the intended page size and orientation.
  • Check the first, middle and last pages of a short report and a multi-page report.
  • Look for clipped text, overlapping elements, unexpected font substitution and broken links.
  • Confirm repeated headers, totals and footnotes appear on the correct pages.
  • Test empty, very long, non-ASCII and malformed values.
  • Store source data separately from generated files so a report can be reproduced.
  • Repeat the checks after changing templates, fonts, ReportLab, WeasyPrint or system dependencies.

Troubleshooting common failures

Rows overlap or text is clipped

Cause: fixed coordinates or widths do not account for wrapping. Fix: use flowables or wrapped paragraph cells in ReportLab, give columns explicit widths, and test long labels. In WeasyPrint, inspect computed widths and print-specific CSS.

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

The table header disappears after page one

Cause: the renderer was not told to repeat it. Fix: use repeatRows=1 in ReportLab or a proper <thead> with print-table testing in WeasyPrint.

Images, styles or fonts are missing

Cause: relative URLs have no usable base path or the runtime cannot access the asset. Fix: provide WeasyPrint’s base_url, use resolvable paths, package assets with the application and test in the deployment environment.

CSS warnings appear

Cause: a property is unsupported or behaves differently in the PDF engine. Fix: consult the documented feature support, replace the property with a supported print rule, and compare the rendered PDF rather than relying on browser preview.

Output changes after a dependency upgrade

Cause: layout, font metrics or supported features changed. Fix: pin and review dependency versions, keep golden PDFs or visual snapshots for representative data, and rerun the verification checklist.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

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

Or skip the browser setup

If your workflow starts with a publicly reachable webpage and you need a PDF or image capture rather than a Python layout engine, ScreenshotNeo provides a single-request API. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the page verdict and billing status in headers.

For PDF capture, use the documented options for paper size, margins, landscape mode and page ranges. You can also wait for a selector, delay or network idle, run custom JavaScript, set cookies or headers, block resources, and capture a specific element. An MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.

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 PDF parameters and response headers. Python and Node.js clients use the same endpoint:

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(`${res.status} ${await res.text()}`);
const data = Buffer.from(await res.arrayBuffer());

The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account to try it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

FAQ

Should the PDF be generated synchronously?

Use a synchronous request for small, interactive reports. For large datasets or batch jobs, queue work and store the result, then notify the caller when validation and rendering finish.

Can one project use both libraries?

Yes. A Python-native financial statement may use ReportLab while a marketing-style appendix uses HTML/CSS. Keep the normalized data model and verification rules shared, and document why each section uses a different renderer.

How do I handle a report that must fit one page?

Set a page-size and content budget first, then control margins, type sizes, column widths and permitted row count. If the data can exceed the budget, define a second-page or continuation policy instead of shrinking text until it is unreadable.

Frequently Asked Questions

Should the PDF be generated synchronously?

Use a synchronous request for small, interactive reports. For large datasets or batch jobs, queue work and store the result, then notify the caller when validation and rendering finish.

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

Can one project use both libraries?

Yes. Share the normalized data model and verification rules, while choosing ReportLab or WeasyPrint per document section.

How do I handle a report that must fit one page?

Set a page-size and content budget first, then define a continuation policy rather than shrinking text until it is unreadable.

The Bottom Line

Normalize and validate first, choose ReportLab or WeasyPrint based on the document’s natural representation, and verify the rendered PDF with realistic data and multi-page cases.

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.

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