How to Generate PDF in Python: A 2026 Guide
You probably started with a simple requirement. Generate an invoice. Export a report. Turn an HTML dashboard into a PDF attachment. On a laptop, that often looks easy.
Then production gets involved.
The same code that worked locally starts failing in a container, fonts render differently on a server, image paths break, or a headless browser bloats your deployment. If you're trying to generate PDF in Python for anything beyond a toy script, the hard part usually isn't Python. It's rendering, packaging, and keeping the pipeline stable after deployment.
Choosing Your Path to PDF Generation in Python
There are really three ways to approach PDF generation in Python.
The first is canvas-style PDF creation. You place text, images, and shapes directly with Python code. That works well for documents with fixed layouts, but it's a poor fit if your team already has HTML templates.
The second is HTML-to-PDF rendering. This is a popular choice because HTML and CSS are easier to maintain than low-level drawing code. It also matches how invoices, reports, and customer-facing documents are usually designed.
The third is offloading rendering to an API. That becomes attractive when deployment headaches start costing more time than the actual document logic.
The production problem most tutorials skip
The biggest gap in real-world Python PDF generation is reliability under deployment constraints. A production PDF report guide for Python notes that Python 3.10+ is now required for one common HTML-to-PDF path, and that the stack still depends on C libraries like Pango and GTK that frequently break in containerized environments.
That matches what many teams run into. Local success doesn't mean cloud success.
Practical rule: pick your PDF approach based on deployment environment first, template complexity second.
If your document is mostly static HTML and CSS, one path fits well. If your page depends on browser behavior and client-side rendering, another path makes more sense. If your service runs in serverless or tightly controlled containers, dependency count matters more than syntax elegance.
How to decide quickly
Use this rough filter:
- Static templates with solid CSS: choose an HTML-first renderer.
- JavaScript-heavy pages: use a browser-based renderer.
- Strict cloud environments: prefer a managed service over local binaries.
- Post-processing existing PDFs: use a PDF manipulation library after rendering.
If you're evaluating the broader document stack around your PDFs, these best documentation platforms for 2026 are a useful reference point because they show how teams are increasingly treating document generation as an operational workflow, not just a file export feature.
For a narrower breakdown focused on implementation patterns, this HTML-to-PDF library guide for Python workflows is also worth reviewing before you commit to one rendering path.
Comparing Python PDF Generation Libraries
The wrong choice here creates years of maintenance. The right choice disappears into the background and keeps shipping PDFs.
The main decision points are simple:
- What renders the document
- How painful the dependencies are
- Whether the source needs real browser behavior
- How much operational complexity your deployment can tolerate
One path is better for clean print-oriented HTML. Another is better when the page behaves like an application. An older path still survives in legacy systems because it was everywhere for years.
Python PDF Generation Library Comparison
| Library | Engine | Dependencies | Modern CSS/JS Support | Best For |
|---|---|---|---|---|
| WeasyPrint | Python renderer with system text/layout libraries | Moderate to high, especially in containers | Strong CSS support, no real browser JavaScript execution | Invoices, reports, printable HTML |
| Playwright | Headless browser | High, includes browser binaries and runtime setup | Best support for modern CSS and JavaScript-driven pages | Dashboards, authenticated web pages, app screenshots as PDF |
| pdfkit with wkhtmltopdf | External command-line renderer | Moderate, binary installation required | Acceptable for older static HTML, weaker for newer layouts | Legacy systems and simple existing templates |
| API-based rendering | Remote managed renderer | Low local dependency burden | Strong, depends on provider implementation | Production pipelines that need reliability over local control |
The trade-offs that matter
For HTML-to-PDF work, there isn't a universal winner.
A Python HTML-to-PDF comparison with layout fidelity and latency data reports that WeasyPrint reaches 98.2% success on CSS3/HTML5 documents and takes about 1.2 seconds per page, while Playwright reaches 99.7% fidelity for JavaScript-heavy pages with 0.4 second latency, but uses 4× more memory and has more dependency failures in containerized deployments.
That single comparison explains most real-world decisions:
- Choose fidelity to standards-based print layouts when your source is controlled HTML and CSS.
- Choose a browser engine when your source behaves like an app, not a document.
- Choose low operational burden when your platform team doesn't want rendering infrastructure inside every deployment artifact.
If your source HTML needs JavaScript to become correct, don't fight that reality with a non-browser renderer.
What I would use in practice
For invoices, statements, and reports generated from templates, I usually want a renderer that respects print CSS and doesn't require me to simulate browser timing issues.
For pages that depend on asynchronous rendering, dynamic charts, or authenticated app state, I want an actual browser because anything else turns into brittle workarounds.
For systems that need boring reliability in cloud environments, I stop caring about local rendering purity and start caring about support burden, package size, and deployment consistency.
If you want a separate take on one alternative implementation route, this guide to HTML-to-PDF with Aspose-style workflows is a useful comparison point.
Generating PDFs from HTML with WeasyPrint
WeasyPrint is the first option I reach for when the input is document-like HTML, not app-like HTML. It handles print styling well, its API is clean, and the output usually looks professional with much less fiddling than older renderers.
The catch is deployment.

Basic example
from weasyprint import HTML
html = """
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: sans-serif;
padding: 24px;
}
h1 {
margin-bottom: 8px;
}
.muted {
color: #666;
}
</style>
</head>
<body>
<h1>Monthly Report</h1>
<p class="muted">Generated from HTML in Python.</p>
<p>This is a simple PDF built from an HTML string.</p>
</body>
</html>
"""
HTML(string=html).write_pdf("report.pdf")
For local development, that's refreshingly simple. If your HTML is clean and your CSS is print-oriented, this path is productive.
A more detailed implementation walkthrough is in this WeasyPrint HTML-to-PDF guide.
Headers, footers, and page numbers
The best part of this workflow is that headers and footers are controlled with CSS, not a separate templating layer.
from weasyprint import HTML
html = """
<!DOCTYPE html>
<html>
<head>
<style>
@page {
size: A4;
margin: 20mm;
@top-center {
content: "Monthly Report";
font-size: 10pt;
color: #555;
}
@bottom-center {
content: "Page " counter(page) " of " counter(pages);
font-size: 10pt;
color: #555;
}
}
body {
font-family: sans-serif;
line-height: 1.5;
}
h1 {
margin-top: 0;
}
</style>
</head>
<body>
<h1>Invoice Summary</h1>
<p>Content goes here.</p>
</body>
</html>
"""
HTML(string=html).write_pdf("invoice-summary.pdf")
WeasyPrint feels like a serious document tool rather than just a converter.
What works and what breaks
What works well:
- Print CSS: page sizing, margins, counters, and predictable document structure
- Static assets: when paths are explicit and the runtime has access to them
- Template-driven output: invoices, statements, certificates, and reports
What causes trouble:
- Container builds: missing system libraries are the classic failure mode
- Fonts: local font assumptions don't survive deployment
- Relative paths: image loading often fails unless asset resolution is handled carefully
- JavaScript-dependent pages: this isn't a browser, so it won't behave like one
Field note: if a PDF needs DOM mutations, delayed rendering, or client-side chart drawing, stop trying to force it through a print renderer.
WeasyPrint is a good engineering choice when the template is under your control. It becomes a frustrating choice when your source is a living web page or when your platform team hates native dependency drift.
Using Headless Browsers with Playwright
When the source page already depends on browser behavior, a headless browser is the honest solution. It renders the page the same way a user sees it, then prints that result to PDF.
That's the big advantage. You don't have to guess how a renderer interprets modern layout or whether it ignores client-side JavaScript. You let the browser do the work.

Basic example with page rendering
from playwright.sync_api import sync_playwright
html = """
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: sans-serif; padding: 40px; }
.card {
border: 1px solid #ddd;
padding: 24px;
border-radius: 8px;
}
</style>
</head>
<body>
<div class="card">
<h1>Sales Snapshot</h1>
<p>Rendered by a headless browser.</p>
</div>
</body>
</html>
"""
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.set_content(html)
page.pdf(path="snapshot.pdf", format="A4", print_background=True)
browser.close()
For app pages, this usually gives fewer surprises than non-browser renderers.
For more implementation details, this Playwright HTML-to-PDF guide is a good companion.
Headers and footers use a different model
Unlike CSS-driven header/footer handling, browser printing often relies on dedicated template options.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com", wait_until="networkidle")
page.pdf(
path="page.pdf",
format="A4",
print_background=True,
display_header_footer=True,
header_template="""
<div style="font-size:10px; width:100%; text-align:center;">
Quarterly Review
</div>
""",
footer_template="""
<div style="font-size:10px; width:100%; text-align:center;">
<span class="pageNumber"></span> / <span class="totalPages"></span>
</div>
"""
)
browser.close()
That works, but it isn't as elegant as print CSS for document-first templates.
Where this shines and where it hurts
A browser-based approach is the right call for:
- Single-page applications
- Pages with client-side charts
- Authenticated internal dashboards
- Templates that already exist as browser pages
It hurts in places that operations teams notice fast:
- Browser binaries add deployment weight
- Cold starts are slower
- Memory use is heavier
- Container stability depends on getting the browser environment right
The value is output fidelity. The cost is infrastructure complexity.
A headless browser is not overkill when your HTML behaves like software. It is overkill when you're printing a static invoice template that could have been rendered with simpler tooling.
The Legacy Option pdfkit and wkhtmltopdf
This stack still shows up in old codebases for one reason. It was the default answer for years.
A lot of teams have scripts, workers, and scheduled jobs built around it, so it hasn't disappeared. If you're maintaining an existing integration, you'll probably run into it. For new systems, I treat it as a compatibility option, not a first choice.
Basic usage
You install the Python wrapper and the external binary, then call it from Python.
import pdfkit
html = """
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: sans-serif; padding: 24px; }
</style>
</head>
<body>
<h1>Legacy PDF Export</h1>
<p>Generated from an HTML string.</p>
</body>
</html>
"""
pdfkit.from_string(html, "legacy.pdf")
You can also render from a file path:
import pdfkit
pdfkit.from_file("template.html", "output.pdf")
Why old systems still keep it
A Python PDF tools tutorial covering wkhtmltopdf usage says the binary remains widely used, with the Python wrapper present in an estimated 85% of legacy Python-based conversion pipelines, typically using 15mm default margins and A4 sizing.
That sounds right for inherited systems. Once a document format has been accepted by finance, legal, or operations, teams hesitate to replace the renderer unless they have to.
Why I wouldn't pick it for a fresh build
The main issues aren't mysterious.
- Rendering age: modern layout support is weaker than newer approaches
- Debugging friction: output bugs can be awkward to diagnose
- Binary management: every environment needs the right executable setup
- Surprise differences: templates that look fine in a browser may print oddly
Old HTML templates can survive on this stack for a long time. New templates shouldn't be designed around its limits.
If your current system already uses it and the PDFs are acceptable, keeping it may be reasonable. But if you're starting clean, you'll spend less time fighting layout edge cases by choosing a more current rendering path.
The Production-Ready Choice The Transformy API
The easiest way to make PDF generation boring in production is to stop rendering locally.
That isn't the most purist answer, but it's the one that often saves the most engineering time. If your application can make an HTTP request, it can generate PDFs without packing browser binaries, native libraries, or image rendering dependencies into every environment.

A minimal Python example
import requests
payload = {
"html": """
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: sans-serif; padding: 32px; }
h1 { margin-bottom: 8px; }
</style>
</head>
<body>
<h1>Customer Report</h1>
<p>Generated through an API call from Python.</p>
</body>
</html>
""",
"options": {
"format": "A4",
"margin": {
"top": "20mm",
"right": "15mm",
"bottom": "20mm",
"left": "15mm"
},
"printBackground": True
}
}
response = requests.post(
"https://transformy.io/api/html-to-pdf",
json=payload,
headers={"Authorization": "Bearer YOUR_API_KEY"},
timeout=60
)
response.raise_for_status()
with open("customer-report.pdf", "wb") as f:
f.write(response.content)
That code is the whole appeal. No browser installation. No native library troubleshooting. No container patching to satisfy rendering dependencies.
Why this fits production better
Python still has broad adoption in automation work. A Python PDF background note on Stack Overflow states that Python holds approximately 15% global market share, and points to long-running document-generation demand through libraries such as ReportLab, which has been developed since 2001. In practice, that same automation focus is exactly why teams move toward managed rendering when cloud reliability matters more than local control.
The gain isn't just fewer install steps. It's a different operational model.
- Deployments stay lean because rendering doesn't live inside the app image
- Cloud environments stay simpler because there are no native graphics stacks to maintain
- Failures are easier to isolate because document generation becomes an external boundary
- Upgrades become less painful because the rendering engine isn't pinned inside your own runtime
When this is the right call
Use an API approach when:
- Your service runs in containers or serverless environments
- You need predictable output across environments
- Your team wants fewer rendering dependencies in app deployments
- PDF generation is infrastructure, not a product differentiator
This is usually the professional choice once PDF generation moves from a feature request to a production responsibility.
If you're comparing managed approaches more broadly, this guide to the best PDF conversion software is a practical next read.
If you need to generate PDF in Python for a one-off script, a local library may be enough. If you need stable output in production, choose the path that reduces operational fragility, not the one with the prettiest demo.
For teams that want the zero-dependency route, the simplest next step is to try the Transformy HTML-to-PDF API.