Generating PDFs from HTML with Python: WeasyPrint and pdfkit
Converting HTML to PDF is the most flexible way to create complex documents: invoices, reports, certificates, and tickets. Python offers three main options: WeasyPrint (pure Python, modern CSS), pdfkit (wkhtmltopdf wrapper), and xhtml2pdf (simpler, no external dependencies).
Tool Comparison
| Tool | Modern CSS | No External Deps | JavaScript | Speed |
|---|---|---|---|---|
| WeasyPrint | Yes (CSS3) | Yes | No | Medium |
| pdfkit (wkhtmltopdf) | Yes (WebKit) | No (binary required) | Yes | Fast |
| xhtml2pdf | Limited | Yes | No | Fast |
| Playwright→PDF | Full Chrome | No (Chromium) | Yes | Slow |
WeasyPrint
Installation
pip install weasyprint
# On Linux, install system dependencies:
apt-get install libpango-1.0-0 libharfbuzz0b libpangoft2-1.0-0
Basic HTML to PDF
import weasyprint
def html_to_pdf(html_content, output_path, base_url=None):
"""
Convert HTML string to PDF.
base_url: base URL to resolve relative paths (CSS, images)
"""
weasyprint.HTML(string=html_content, base_url=base_url).write_pdf(output_path)
print(f"PDF generated: {output_path}")
html = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; margin: 2cm; }
h1 { color: #2E5984; border-bottom: 2px solid #2E5984; }
table { width: 100%; border-collapse: collapse; }
th { background: #2E5984; color: white; padding: 8px; }
td { padding: 6px; border-bottom: 1px solid #ddd; }
</style>
</head>
<body>
<h1>Sales Report</h1>
<p>Generated: April 24, 2026</p>
<table>
<tr><th>Product</th><th>Qty</th><th>Total</th></tr>
<tr><td>Laptop Pro</td><td>3</td><td>$3,899.97</td></tr>
<tr><td>4K Monitor</td><td>5</td><td>$2,745.00</td></tr>
</table>
</body>
</html>
"""
html_to_pdf(html, "report.pdf")
From HTML File + External CSS
import weasyprint
def file_to_pdf(html_path, output_path, css_path=None):
html = weasyprint.HTML(filename=str(html_path))
stylesheets = []
if css_path:
stylesheets.append(weasyprint.CSS(filename=str(css_path)))
html.write_pdf(str(output_path), stylesheets=stylesheets)
print(f"PDF: {output_path}")
file_to_pdf("invoice.html", "invoice.pdf", "pdf_styles.css")
Headers and Footers with CSS @page
html_with_header = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
@page {
size: A4;
margin: 2.5cm 2cm 3cm 2cm;
@top-center {
content: "My Company Inc. — Confidential";
font-size: 9pt;
color: #666;
}
@bottom-left {
content: "Generated: 04/24/2026";
font-size: 8pt;
color: #888;
}
@bottom-center {
content: "Page " counter(page) " of " counter(pages);
font-size: 9pt;
}
@bottom-right {
content: "www.mycompany.com";
font-size: 8pt;
color: #888;
}
}
body { font-family: "Helvetica Neue", Arial, sans-serif; font-size: 11pt; }
h1 { page-break-before: avoid; }
table { page-break-inside: avoid; }
.new-page { page-break-before: always; }
</style>
</head>
<body>
<h1>Annual Report 2026</h1>
<p>Report content here...</p>
<div class="new-page">
<h1>Appendix</h1>
<p>Additional data...</p>
</div>
</body>
</html>
"""
import weasyprint
weasyprint.HTML(string=html_with_header).write_pdf("report_with_header.pdf")
Professional Invoice with WeasyPrint + Jinja2
from jinja2 import Template
import weasyprint
from datetime import date
INVOICE_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
@page { size: A4; margin: 1.5cm; }
body { font-family: Arial, sans-serif; font-size: 10pt; color: #333; }
.header { display: flex; justify-content: space-between; margin-bottom: 2em; }
.logo { font-size: 24pt; font-weight: bold; color: #2E5984; }
.inv-no { font-size: 18pt; font-weight: bold; color: #666; }
.client { background: #f5f5f5; padding: 1em; margin: 1em 0; border-radius: 4px; }
table { width: 100%; border-collapse: collapse; margin: 1.5em 0; }
th { background: #2E5984; color: white; padding: 8px; text-align: left; }
td { padding: 6px 8px; border-bottom: 1px solid #e0e0e0; }
.total-row { font-weight: bold; background: #f5f5f5; }
.amount { text-align: right; }
</style>
</head>
<body>
<div class="header">
<div>
<div class="logo">{{ company.name }}</div>
<div>{{ company.address }}<br>
EIN: {{ company.ein }}<br>
{{ company.email }}</div>
</div>
<div class="inv-no">
INVOICE #{{ number }}<br>
<small>Date: {{ date }}</small>
</div>
</div>
<div class="client">
<strong>Bill to:</strong><br>
{{ client.name }}<br>
{{ client.address }}
</div>
<table>
<tr>
<th>Description</th><th>Qty</th>
<th class="amount">Unit Price</th><th class="amount">Total</th>
</tr>
{% for line in lines %}
<tr>
<td>{{ line.description }}</td>
<td>{{ line.qty }}</td>
<td class="amount">${{ "%.2f"|format(line.price) }}</td>
<td class="amount">${{ "%.2f"|format(line.qty * line.price) }}</td>
</tr>
{% endfor %}
<tr><td colspan="3">Subtotal</td><td class="amount">${{ "%.2f"|format(subtotal) }}</td></tr>
<tr><td colspan="3">Tax (10%)</td><td class="amount">${{ "%.2f"|format(tax) }}</td></tr>
<tr class="total-row">
<td colspan="3">TOTAL</td><td class="amount">${{ "%.2f"|format(total) }}</td>
</tr>
</table>
<p style="font-size: 9pt; color: #888;">
Payment: Bank transfer — Net 30 days from invoice date
</p>
</body>
</html>
"""
def generate_invoice(data, output_path):
lines = data['lines']
subtotal = sum(l['qty'] * l['price'] for l in lines)
tax = subtotal * 0.10
total = subtotal + tax
html = Template(INVOICE_TEMPLATE).render(
**data, subtotal=subtotal, tax=tax, total=total,
date=date.today().strftime("%m/%d/%Y"),
)
weasyprint.HTML(string=html).write_pdf(output_path)
print(f"Invoice generated: {output_path}")
generate_invoice({
"number": "INV-2026-042",
"company": {
"name": "TechSolutions Inc.",
"ein": "12-3456789",
"address": "123 Main St, San Francisco, CA 94105",
"email": "billing@techsolutions.com",
},
"client": {
"name": "Client Company LLC",
"address": "456 Oak Ave, New York, NY 10001",
},
"lines": [
{"description": "Web development (40h)", "qty": 40, "price": 85.00},
{"description": "Annual hosting", "qty": 1, "price": 300.00},
{"description": "Monthly maintenance", "qty": 3, "price": 200.00},
],
}, "invoice_INV-2026-042.pdf")
pdfkit (wkhtmltopdf)
pip install pdfkit
# Install wkhtmltopdf:
# Ubuntu: apt-get install wkhtmltopdf
# Windows: download from https://wkhtmltopdf.org/downloads.html
import pdfkit
options = {
'page-size': 'A4',
'margin-top': '2cm',
'margin-right': '1.5cm',
'margin-bottom': '2.5cm',
'margin-left': '1.5cm',
'encoding': 'UTF-8',
'footer-center': 'Page [page] of [topage]',
'footer-font-size': '8',
'enable-local-file-access': None,
}
# From HTML string
pdfkit.from_string("<h1>Hello World</h1>", "output.pdf", options=options)
# From URL
pdfkit.from_url("https://example.com", "page.pdf", options=options)
# From file
pdfkit.from_file("report.html", "report.pdf", options=options)
xhtml2pdf (No External Dependencies)
pip install xhtml2pdf
from xhtml2pdf import pisa
import io
def html_to_pdf_xhtml2pdf(html_content, output_path):
with open(output_path, "wb") as f:
result = pisa.CreatePDF(
io.StringIO(html_content),
dest=f,
encoding='utf-8'
)
if result.err:
print(f"Error: {result.err}")
else:
print(f"PDF generated: {output_path}")
html_to_pdf_xhtml2pdf("""
<html><body>
<h1 style="color: blue">Title</h1>
<p>Document text here.</p>
</body></html>
""", "simple.pdf")
Additional Resource
For converting HTML or Word documents to PDF without any coding, use KaijuConverter — free, fast, and no registration required.
Related conversions
Document conversions that follow this topic naturally: