Alright, let’s talk about making PDFs. It’s a task that often feels like trying to build a ship in a bottle: you know exactly what you want the final, beautiful, static thing to look like, but the process of getting there is fiddly, frustrating, and involves a lot of swearing. You’re not just writing a document; you’re doing low-level typesetting, painting a canvas with code. For this, in the Python world, you have two main contenders: the old, powerful, and occasionally cantankerous reportlab, and the modern, web-standard-savvy weasyprint.

The Old Guard: reportlab

reportlab is the industry heavyweight. It’s been around forever, it’s incredibly powerful, and it gives you fine-grained control over every single point on the page. The trade-off? Its API is… let’s call it “idiosyncratic.” It feels like it was designed by brilliant engineers who were deeply suspicious of anyone who hadn’t also written a PostScript interpreter from scratch.

The core concept is the “canvas.” You get a blank page and you drawString text or drawImage onto it at precise (x,y) coordinates. It’s manual, but that means you can build literally anything. For anything more structured, you use “Flowables” – things like Paragraphs, Tables, and Lists that can be… well… flowed into a document frame. It’s a powerful two-tiered system.

Let’s generate a simple “Hello World” PDF. You’ll need to pip install reportlab first.

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

# Create a canvas object, specifying the filename and page size
c = canvas.Canvas("hello_reportlab.pdf", pagesize=letter)

# Set the font. You have to do this. It won't guess.
c.setFont("Helvetica", 12)

# Draw a string 1 inch from the left and 10 inches from the bottom
# Wait, why is the Y-coordinate from the BOTTOM? Because PDFs, that's why.
c.drawString(1 * inch, 10 * inch, "Hello from ReportLab. You are at my mercy.")

# Don't forget to save! This actually writes the file.
c.save()

See? You’re basically telling a plotter where to move its pen. The power is immense, but so is the potential for frustration. Want to center text? Get out your calculator: (width_of_page - width_of_string) / 2. And heaven help you if you change the font size and forget to recalculate.

Pitfalls & Best Practices:

  • Fonts: You must manage fonts yourself. setFont will fail silently if you don’t have the font. For production, embed subsets of fonts.
  • Stateful: The canvas is stateful. Your current font, color, and even position are maintained until you change them. This is a common source of bugs where a style from one part of the code leaks into another.
  • Use Platypus: For any real document (invoices, reports), don’t use the raw canvas. Use the PLATYPUS framework (Page Layout and Typography Using Scripts) with Flowables. It handles pagination, frames, and styling for you. It’s complex, but less maddening.

The Modernist: WeasyPrint

weasyprint takes a completely different, and frankly, more delightful approach. Its core philosophy is: “You already know how to design a document. It’s called HTML and CSS.” You build a well-structured HTML document, style it with CSS—including support for CSS Paged Media for controlling margins, page breaks, and even generating page headers/footers—and then feed it to weasyprint to get a pristine PDF.

It’s a revelation. You can prototype your document in a browser and then seamlessly generate it as a PDF. Need to change the font? It’s one line in your CSS. Need a header on every page? Use @page rules. It’s glorious.

# First, pip install weasyprint

from weasyprint import HTML

# This can be a URL, a file path, or a string containing HTML
html_string = """
<!DOCTYPE html>
<html>
<head>
    <style>
        body { font-family: sans-serif; color: #333; }
        h1 { color: #0066cc; }
        .urgent { background-color: #ffcccc; padding: 1em; }
        @page {
            size: letter;
            margin: 1in;
            @top-right {
                content: "Page " counter(page);
                font-size: 10pt;
            }
        }
    </style>
</head>
<body>
    <h1>My Beautiful, Standards-Compliant Report</h1>
    <p>This was so much easier than calculating coordinates.</p>
    <div class="urgent">Note: WeasyPrint is objectively cooler.</div>
</body>
</html>
"""

# Generate the PDF
HTML(string=html_string).write_pdf('hello_weasyprint.pdf')

Isn’t that just cleaner? You’re describing what you want, not micromanaging how to achieve it.

Pitfalls & Best Practices:

  • Not a Web Browser: It uses a custom CSS engine for print output. It doesn’t support JavaScript, Web APIs, or highly dynamic content. It’s for generating documents, not archiving web pages.
  • Font Handling: While easier than reportlab, you still need to ensure the fonts you reference in your CSS are available on the system where the code runs, or use @font-face to embed web fonts.
  • The Missing 20%: It implements most of the CSS Paged Media spec, but not all. You might occasionally bump into a property that isn’t supported. Always check the documentation.

So, Which One Do You Use?

This isn’t a hard choice. Use weasyprint unless you can’t. If your PDF is a document—a report, an invoice, a letter, a book—weasyprint is the right tool. It’s faster to develop, easier to maintain, and leverages a skillset (HTML/CSS) that is far more common than reportlab’s proprietary system.

You only need to drop down to reportlab if you need pixel-perfect, programmatic control for something like a custom barcode, a complex vector graphic, or a pre-printed form where text must be placed at exact coordinates. It’s the power tool for when the job demands it, but for 90% of tasks, weasyprint is the elegant solution you’ve been waiting for.