pdforge logo

pdforge

Product

Resources

Integrations

PDF Libraries

PDF Libraries

Python

Python

How to Generate PDF Using ReportLab in Python (Updated 2025)

Marcelo Abreu, founder of pdforge

Marcelo | Founder

Marcelo | Founder

Oct 15, 2025

Oct 15, 2025

An Introduction to ReportLab: A Python PDF Generation Library

ReportLab is an open-source library for PDF generation in Python, known for its flexibility and precision when building complex, data-driven documents. It allows developers to create PDFs programmatically from scratch, offering deep customization for layouts, fonts, graphics, and tables.

Because of its powerful API, ReportLab is widely used in SaaS platforms and enterprise systems that require dynamic PDF creation, from invoices and financial statements to dashboards and analytical reports. Its robust, open-source foundation makes it ideal for developers who want full control over document design, going far beyond simple HTML-to-PDF conversion.

You can access the official documentation here.

Comparing ReportLab with other PDF Generation Python Libraries

When it comes to Python PDF generation, ReportLab stands out for its programmatic control and customization. It’s often compared with other libraries such as Pyppeteer, PyPDF2, Python PDFKit, and Playwright, each serving different needs depending on the type of PDF workflow you’re building.

Number os montly downloads for reportlab

ReportLab: Full Control and Customization

ReportLab allows you to create PDFs from scratch using Python objects, giving developers precise control over layouts, fonts, tables, and vector graphics. This makes it ideal for applications that need custom report generation or dynamically generated invoices and dashboards.

Pyppeteer and Playwright: HTML Rendering Engines

Both Pyppeteer and Playwright use a headless Chromium engine to render HTML pages into PDFs, ensuring pixel-perfect fidelity to your web designs. These tools are perfect when you already have an HTML template or need to export web reports directly as PDFs. However, they provide less flexibility for programmatic layouts or template-based design.

PDFKit: Simple HTML to PDF Conversion

PDFKit acts as a wrapper around wkhtmltopdf, offering a simple API for converting HTML or URLs to PDFs. It’s fast and easy to use but limited in styling and dynamic content control compared to ReportLab or Playwright.

PyPDF2: Manipulating Existing PDFs

PyPDF2 focuses on post-processing tasks such as merging, splitting, and encrypting PDFs. It’s not designed to generate documents from scratch, but rather to modify or combine existing files.


If your goal is to generate PDFs in Python with custom layouts, fonts, and data-driven content, ReportLab is the best choice. If you’d like a deeper analysis, check out our detailed guide comparing ReportLab, Playwright, and other Python PDF libraries in 2025.

Guide to generate pdf from html using python reportlab
Guide to generate pdf from html using python reportlab

Setting Up the Environment for PDF Generation with ReportLab

To start working with ReportLab, we need to install the necessary dependencies and create a project setup.

Installing ReportLab and Required Dependencies

Install the core ReportLab library using pip:

If you plan to parse or extract content from HTML before generating the PDF, also install lxml:

💡 Tip: ReportLab doesn’t convert HTML directly into PDFs like Playwright or PDFKit. Instead, you use Python objects (Paragraphs, Tables, Images, Drawings) to build PDFs programmatically.

A Quick Overview of Python ReportLab PDF Generation

ReportLab’s power lies in constructing PDFs programmatically, not just rendering static HTML.

  • You create elements like Paragraphs, Tables, and Images using Python objects.

  • You then build them into a “story” (a list of flowable components).

  • ReportLab handles pagination, layout, and export to PDF automatically.

Here’s a minimal “Hello PDF” to test your setup:

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

pdf = canvas.Canvas("hello.pdf", pagesize=A4)
pdf.setFont("Helvetica", 14)
pdf.drawString(100, 750, "Hello, ReportLab PDF Generation!")
pdf.save()

Run it:

✅ You’ll find a hello.pdf in your folder — proof that ReportLab is working.

Next, let’s dive into how to use ReportLab to generate fully structured, styled documents, including tables, charts, and HTML-inspired layouts.

Step-by-Step Guide: Create your PDF document using ReportLab

Before we start, install the packages:

We will cover both low-level canvas APIs and the Platypus high-level layout engine. This makes it easy to create a PDF in Python with ReportLab, from simple pages to multi-page reports.

Step one: Creating the PDF Canvas with basic components

The Canvas API is the most direct way to draw on a PDF. You control coordinates, fonts, colors, and shapes. This is perfect when you want pixel-precise placement.

1.1 Create a blank PDF and draw text and shapes

Heres the reportlab code example for it:

from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib import colors

def create_basic_canvas_pdf(path="basic_canvas.pdf"):
    c = canvas.Canvas(path, pagesize=A4)
    width, height = A4  # points

    # Title
    c.setFont("Helvetica-Bold", 20)
    c.drawString(20 * mm, height - 20 * mm, "ReportLab PDF Generation: Canvas Example")

    # Body text
    c.setFont("Helvetica", 12)
    c.drawString(20 * mm, height - 30 * mm, "Hello from Python. This PDF was created using ReportLab's canvas.")

    # Line
    c.setStrokeColor(colors.HexColor("#333333"))
    c.setLineWidth(1.2)
    c.line(20 * mm, height - 35 * mm, width - 20 * mm, height - 35 * mm)

    # Rectangle
    c.setFillColor(colors.HexColor("#F5F7FA"))
    c.rect(20 * mm, height - 80 * mm, width - 40 * mm, 35 * mm, fill=1, stroke=0)

    # A simple footer
    c.setFillColor(colors.black)
    c.setFont("Helvetica-Oblique", 9)
    c.drawRightString(width - 20 * mm, 10 * mm, "Generated with ReportLab Python")

    c.showPage()
    c.save()

if __name__ == "__main__":
    create_basic_canvas_pdf()

What this does:

  • Sets up A4 page size.

  • Draws a title, a divider line, and a background rectangle.

  • Adds a footer.

This is the most direct ReportLab create PDF approach.

1.2 Draw an image and automatic page breaks

Heres the code example:

from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm

def canvas_with_image(path="canvas_with_image.pdf", logo_path="logo.png"):
    c = canvas.Canvas(path, pagesize=A4)
    width, height = A4

    c.setFont("Helvetica-Bold", 16)
    c.drawString(20 * mm, height - 20 * mm, "Canvas with Image")

    # Draw an image at fixed size
    c.drawImage(logo_path, 20 * mm, height - 60 * mm, width=30 * mm, height=30 * mm, preserveAspectRatio=True, mask='auto')

    # Force a new page when needed
    c.showPage()
    c.drawString(20 * mm, height - 20 * mm, "Second Page")
    c.save()

Step two: Using more advanced components like tables or charts

For structured content that flows across pages, use Platypus. It provides Flowables such as Paragraph, Table, Image, and Spacer.

2.1 Build a multi-page document with Platypus

Heres how to do it:

from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image, PageBreak
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors
from reportlab.lib.units import mm

def build_invoice_pdf(path="invoice_platypus.pdf", logo_path=None):
    doc = SimpleDocTemplate(
        path,
        pagesize=A4,
        rightMargin=20 * mm,
        leftMargin=20 * mm,
        topMargin=20 * mm,
        bottomMargin=20 * mm,
    )
    styles = getSampleStyleSheet()
    styles.add(ParagraphStyle(name="Small", fontName="Helvetica", fontSize=9, leading=12))

    story = []

    # Optional logo
    if logo_path:
        story.append(Image(logo_path, width=30 * mm, height=30 * mm))
        story.append(Spacer(1, 6 * mm))

    story.append(Paragraph("Invoice", styles["Title"]))
    story.append(Spacer(1, 4 * mm))

    meta = [
        ["Invoice #", "12345"],
        ["Date", "2024-10-07"],
        ["Client", "John Doe"],
        ["Due Date", "2024-11-07"],
    ]
    meta_tbl = Table(meta, colWidths=[30 * mm, 60 * mm])
    meta_tbl.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#F0F0F0")),
        ("BOX", (0, 0), (-1, -1), 0.5, colors.HexColor("#DDDDDD")),
        ("INNERGRID", (0, 0), (-1, -1), 0.25, colors.HexColor("#DDDDDD")),
        ("ALIGN", (0, 0), (-1, -1), "LEFT"),
        ("FONT", (0, 0), (-1, -1), "Helvetica", 10),
    ]))
    story.append(meta_tbl)
    story.append(Spacer(1, 6 * mm))

    # Line items
    data = [
        ["Item", "Price"],
        ["Website Design", "$300.00"],
        ["Hosting (3 months)", "$75.00"],
    ]
    tbl = Table(data, colWidths=[100 * mm, 40 * mm])
    tbl.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#EEEEEE")),
        ("TEXTCOLOR", (0, 0), (-1, 0), colors.HexColor("#333333")),
        ("ALIGN", (1, 1), (-1, -1), "RIGHT"),
        ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
        ("BOTTOMPADDING", (0, 0), (-1, 0), 8),
        ("GRID", (0, 0), (-1, -1), 0.25, colors.HexColor("#CCCCCC")),
    ]))
    story.append(tbl)
    story.append(Spacer(1, 6 * mm))

    story.append(Paragraph("<b>Total</b>: $375.00", styles["Normal"]))
    story.append(Spacer(1, 10 * mm))

    # Page break and terms
    story.append(PageBreak())
    story.append(Paragraph("Terms and Conditions", styles["Heading2"]))
    story.append(Paragraph(
        "Payment is due within 30 days. Please contact billing@example.com for questions.",
        styles["Small"]
    ))

    doc.build(story)

if __name__ == "__main__":
    build_invoice_pdf()

2.2 Add a simple chart (Bar chart) with ReportLab Graphics

ReportLab includes charting in reportlab.graphics. You can embed charts as Flowables.

from reportlab.graphics.shapes import Drawing
from reportlab.graphics.charts.barcharts import VerticalBarChart
from reportlab.graphics.widgets.markers import makeMarker
from reportlab.lib.colors import HexColor
from reportlab.platypus import SimpleDocTemplate, Spacer
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm

def bar_chart_example(path="chart_example.pdf"):
    doc = SimpleDocTemplate(path, pagesize=A4)
    story = []

    d = Drawing(400, 200)

    data = [
        (300, 120, 90, 160, 200),  # Sales
    ]

    bc = VerticalBarChart()
    bc.x = 30
    bc.y = 40
    bc.height = 120
    bc.width = 320
    bc.data = data
    bc.strokeColor = HexColor("#333333")
    bc.barWidth = 20
    bc.groupSpacing = 10
    bc.categoryAxis.categoryNames = ["Jan", "Feb", "Mar", "Apr", "May"]
    bc.valueAxis.valueMin = 0
    bc.valueAxis.valueMax = 350
    bc.valueAxis.valueStep = 50

    # Optional marker on bars
    bc.bars[0].symbol = makeMarker("Circle")

    d.add(bc)
    story.append(d)
    story.append(Spacer(1, 10 * mm))

    doc.build(story)

if __name__ == "__main__":
    bar_chart_example()

Step three: Fix Fonts, Styling, Margins, Headers and Footers

To get professional output, set typography, page margins, and consistent headers and footers.

3.1 Register and use custom fonts

Here's how to do it:

from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet

def register_fonts():
    pdfmetrics.registerFont(TTFont("Inter", "Inter-Regular.ttf"))
    pdfmetrics.registerFont(TTFont("Inter-Bold", "Inter-Bold.ttf"))

    styles = getSampleStyleSheet()
    styles.add(ParagraphStyle(name="InterBody", fontName="Inter", fontSize=11, leading=14))
    styles.add(ParagraphStyle(name="InterHeading", fontName="Inter-Bold", fontSize=16, leading=20))
    return styles

Tip: package the font files with your app or host them internally. This improves brand consistency for ReportLab PDF generation.

3.2 Add headers and footers with page callbacks

With SimpleDocTemplate, you can pass onFirstPage and onLaterPages to draw headers and footers on every page.

from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.pdfgen.canvas import Canvas

def _header_footer(canvas: Canvas, doc):
    width, height = A4
    canvas.saveState()
    # Header
    canvas.setFont("Helvetica", 9)
    canvas.drawString(20 * mm, height - 10 * mm, "Sample Company • Python ReportLab Generate PDF")
    # Footer with page number
    page_num = canvas.getPageNumber()
    canvas.drawRightString(width - 20 * mm, 10 * mm, f"Page {page_num}")
    canvas.restoreState()

def doc_with_header_footer(path="header_footer.pdf"):
    doc = SimpleDocTemplate(path, pagesize=A4, leftMargin=20*mm, rightMargin=20*mm, topMargin=20*mm, bottomMargin=20*mm)
    styles = getSampleStyleSheet()
    story = [Paragraph("Header and Footer Example", styles["Title"]), Spacer(1, 12)]
    story += [Paragraph("Content goes here. " * 30, styles["Normal"]) for _ in range(20)]
    doc.build(story, onFirstPage=_header_footer, onLaterPages=_header_footer)

if __name__ == "__main__":
    doc_with_header_footer()

PDF Examples for Python ReportLab Code

Below are two important patterns that many developers need when they search for reportlab python pdf example: page templates with frames, and mapping simple HTML to ReportLab flowables.

Using PageTemplates and DocTemplates

For complex layouts, use BaseDocTemplate + Frame + PageTemplate. This gives granular control over columns, margins, and repeating elements.

from reportlab.platypus import BaseDocTemplate, PageTemplate, Frame, Paragraph, Spacer
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.pdfgen.canvas import Canvas

def header(canvas: Canvas, doc):
    width, height = A4
    canvas.saveState()
    canvas.setFont("Helvetica", 9)
    canvas.drawString(20 * mm, height - 10 * mm, "ReportLab PageTemplate Example")
    canvas.restoreState()

def footer(canvas: Canvas, doc):
    width, height = A4
    canvas.saveState()
    canvas.setFont("Helvetica-Oblique", 9)
    canvas.drawRightString(width - 20 * mm, 10 * mm, f"Page {canvas.getPageNumber()}")
    canvas.restoreState()

def build_with_templates(path="doc_templates.pdf"):
    doc = BaseDocTemplate(path, pagesize=A4)

    # Define frames (margins and columns)
    frame_main = Frame(20 * mm, 20 * mm, 170 * mm, 257 * mm, id='main')  # x, y, width, height

    # Page template with header and footer
    template = PageTemplate(id='main_template', frames=[frame_main],
                            onPage=header, onPageEnd=footer)
    doc.addPageTemplates([template])

    styles = getSampleStyleSheet()
    story = [Paragraph("Using PageTemplates and DocTemplates", styles["Title"]), Spacer(1, 6 * mm)]
    for i in range(50):
        story.append(Paragraph(f"Paragraph {i+1}. This shows flowing content inside a Frame.", styles["Normal"]))
        story.append(Spacer(1, 3 * mm))

    doc.build(story)

if __name__ == "__main__":
    build_with_templates()

Why this matters:

  • You get magazine-like layouts.

  • You can add multiple frames for multi-column pages.

  • Headers and footers do not interfere with body frames.

Converting HTML to PDF using ReportLab

Important: ReportLab does not fully render arbitrary HTML and CSS like a browser. The Paragraph flowable supports a subset of inline markup such as <b>, <i>, <u>, <font>, <br/>, and simple lists. For more control, parse your HTML and map elements to Flowables.

1. Minimal example using supported inline markup

from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet

def html_like_paragraphs(path="html_like.pdf"):
    doc = SimpleDocTemplate(path)
    styles = getSampleStyleSheet()
    story = []

    html_snippet = """
    <b>Invoice</b><br/>
    <font size="10">Client: John Doe</font><br/>
    <i>Date: 2024-10-07</i><br/>
    """
    story.append(Paragraph(html_snippet, styles["Normal"]))
    story.append(Spacer(1, 12))
    story.append(Paragraph("Line items and totals go below.", styles["Normal"]))

    doc.build(story)

if __name__ == "__main__":
    html_like_paragraphs()

2. Map a real HTML invoice to Flowables using lxml

This pattern reads your HTML and converts elements into ReportLab flowables. It is a common approach for python reportlab generate pdf when you have HTML sources.

from lxml import etree, html
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm

def html_invoice_to_pdf(html_path="html_templates/invoice.html", out_path="invoice_from_html.pdf"):
    with open(html_path, "r", encoding="utf-8") as f:
        html_content = f.read()

    tree = html.fromstring(html_content)
    styles = getSampleStyleSheet()
    doc = SimpleDocTemplate(out_path, pagesize=A4, leftMargin=20*mm, rightMargin=20*mm, topMargin=20*mm, bottomMargin=20*mm)
    story = []

    # Title
    h1 = tree.xpath("//h1/text()")
    if h1:
        story.append(Paragraph(h1[0], styles["Title"]))
        story.append(Spacer(1, 6 * mm))

    # Invoice meta (example mapping for two-column rows)
    meta_rows = []
    for tr in tree.xpath("//table[contains(@class, 'details')][1]//tr"):
        tds = [td.text_content().strip() for td in tr.xpath("./td")]
        if len(tds) == 2:
            meta_rows.append([tds[0], tds[1]])
    if meta_rows:
        meta_tbl = Table(meta_rows, colWidths=[80 * mm, 80 * mm])
        meta_tbl.setStyle(TableStyle([
            ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#F7F7F7")),
            ("BOX", (0, 0), (-1, -1), 0.5, colors.HexColor("#DDDDDD")),
            ("INNERGRID", (0, 0), (-1, -1), 0.25, colors.HexColor("#DDDDDD")),
            ("FONT", (0, 0), (-1, -1), "Helvetica", 10),
        ]))
        story.append(meta_tbl)
        story.append(Spacer(1, 6 * mm))

    # Items table (assuming the second .details table contains items)
    item_table = tree.xpath("//table[contains(@class, 'details')][2]")
    if item_table:
        rows = []
        for tr in item_table.xpath(".//tr"):
            cells = [td.text_content().strip() for td in tr.xpath("./td")]
            if cells:
                rows.append(cells)
        item_tbl = Table(rows, colWidths=[100 * mm, 40 * mm])
        item_tbl.setStyle(TableStyle([
            ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#EEEEEE")),
            ("GRID", (0, 0), (-1, -1), 0.25, colors.HexColor("#CCCCCC")),
            ("ALIGN", (1, 1), (-1, -1), "RIGHT"),
            ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
        ]))
        story.append(item_tbl)
        story.append(Spacer(1, 6 * mm))

    # Totals
    total_text = tree.xpath("//table[contains(@class, 'totals')]//strong/parent::td/text()")
    if total_text:
        story.append(Paragraph(f"<b>Total:</b> {total_text[0].strip()}", styles["Normal"]))

    doc.build(story)

if __name__ == "__main__":
    html_invoice_to_pdf()

Notes and best practices:

  • Keep your HTML predictable. Use classes that are easy to select with XPath.

  • Map each HTML region to the appropriate Flowable.

  • For complex HTML and full CSS fidelity, consider Playwright or Pyppeteer to render HTML as PDF. Use ReportLab when you need programmatic control and data-driven layout.

Alternative: Convert HTML to PDF at Scale Using pdforge

Homepage of pdforge

Managing HTML to PDF conversion at scale poses challenges, particularly with serverless architectures (see our detailed article) and frequent template updates. pdforge simplifies these challenges by providing a robust PDF Generation API for direct HTML to PDF conversion:

import requests

url = 'https://api.pdforge.com/v1/html-to-pdf/sync'
headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_API_KEY'
}
payload = {
    'html': '<h1>Invoice</h1><p>Customer details here</p>'
}

response = requests.post(url, headers=headers, json=payload)

pdforge also includes a powerful AI Agent that generates PDF templates instantly and a modern no-code editor for quick design fine-tuning. Its intuitive platform allows non-developers to manage templates efficiently. Here's a quick demo on how it works:

You can create your account, experience our no-code builder and create your first layout template without any upfront payment clicking here.

Conclusion

ReportLab remains one of the most powerful tools in the Python ecosystem for custom PDF generation. Its programmatic approach gives developers full control over every element: layouts, fonts, tables, and charts, making it ideal for building dynamic, data-driven, and branded documents at scale.

While libraries like Pyppeteer and Playwright excel at HTML-to-PDF rendering and PyPDF2 is best for manipulating existing PDFs, ReportLab shines when you need to create PDFs from scratch with precision and flexibility. It’s the preferred choice for developers who want structure and style to come directly from Python logic rather than static HTML templates.

Choose third-party pdf generation APIs, like pdforge, if you don't want to waste time maintaining pdfs layouts and their infrastructure or if you want to leverage an AI-first platform that designs the PDF templates for you so you don't have to keep track of best practices to generate PDFs at scale.

Generating pdfs at scale can be annoying!

Let us help you make it easier while you focus on what truly matters for your company.

pdforge logo
pattern behind Call to action

Generating pdfs at scale can be annoying!

Let us help you make it easier while you focus on what truly matters for your company.

pdforge logo
pattern behind Call to action

Table of contents

Automate PDF Generation with pdforge

No code or design experience needed

AI creates your template in seconds

Fine tune the design in our no-code builder

Generate PDFs with our API or integrations