Method 1: WeasyPrint (Pure Python)
WeasyPrint renders HTML+CSS to PDF using Python's own rendering engine — no browser binary required. It handles most CSS well but doesn't execute JavaScript, so it's best for server-rendered HTML and templates.
pip install weasyprint --break-system-packages
from weasyprint import HTML, CSS
# From URL
HTML('https://example.com').write_pdf('output.pdf')
# From HTML string
html_content = '''
<html>
<head><style>
@page { size: A4; margin: 2cm; }
body { font-family: Arial, sans-serif; font-size: 12pt; }
h1 { color: #333; border-bottom: 2px solid #7c6aff; padding-bottom: 8px; }
table { width: 100%; border-collapse: collapse; }
td, th { border: 1px solid #ddd; padding: 8px; }
</style></head>
<body>
<h1>Invoice #1042</h1>
<table>
<tr><th>Item</th><th>Price</th></tr>
<tr><td>SnapAPI Pro</td><td>$79/mo</td></tr>
</table>
</body>
</html>
'''
HTML(string=html_content).write_pdf('invoice.pdf')
# Return bytes (for Django/Flask response)
pdf_bytes = HTML(string=html_content).write_pdf()
WeasyPrint with Django Templates
# views.py
from django.http import HttpResponse
from django.template.loader import render_to_string
from weasyprint import HTML
import tempfile
def invoice_pdf(request, invoice_id):
invoice = Invoice.objects.get(pk=invoice_id)
html_string = render_to_string('invoices/invoice.html', {'invoice': invoice})
pdf_bytes = HTML(string=html_string, base_url=request.build_absolute_uri('/')).write_pdf()
response = HttpResponse(pdf_bytes, content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="invoice-{invoice_id}.pdf"'
return response
Method 2: pdfkit + wkhtmltopdf
pdfkit wraps wkhtmltopdf, which uses a deprecated WebKit engine. It's widely used but wkhtmltopdf was abandoned in 2023. Fine for simple HTML, problematic for modern CSS (flexbox issues, Grid support gaps).
pip install pdfkit --break-system-packages
# Also needs system binary:
brew install wkhtmltopdf # macOS
apt-get install wkhtmltopdf # Ubuntu
import pdfkit
# From URL
pdfkit.from_url('https://example.com', 'output.pdf')
# From HTML string
pdfkit.from_string('<h1>Hello</h1>', 'output.pdf', options={
'page-size': 'A4',
'margin-top': '2cm',
'margin-right': '1.5cm',
'margin-bottom': '2cm',
'margin-left': '1.5cm',
'encoding': 'UTF-8',
'no-outline': None
})
Method 3: Playwright (Full Chrome Rendering)
For JavaScript-heavy pages — dashboards, SPAs, pages with charts — Playwright drives a real Chromium instance for accurate PDF output.
pip install playwright --break-system-packages
python -m playwright install chromium
from playwright.sync_api import sync_playwright
def url_to_pdf(url: str, output_path: str) -> bytes:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto(url, wait_until='networkidle')
# Add print-specific CSS
page.add_style_tag(content='''
@page { margin: 2cm; }
.no-print, nav, .sidebar { display: none !important; }
* { box-shadow: none !important; }
''')
pdf = page.pdf(
format='A4',
print_background=True,
margin={'top': '2cm', 'right': '1.5cm', 'bottom': '2cm', 'left': '1.5cm'},
display_header_footer=True,
header_template='',
footer_template='Page of '
)
browser.close()
with open(output_path, 'wb') as f:
f.write(pdf)
return pdf
url_to_pdf('https://example.com', 'output.pdf')
Async Playwright PDF (FastAPI / async Django)
import asyncio
from playwright.async_api import async_playwright
async def url_to_pdf_async(url: str) -> bytes:
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
await page.goto(url, wait_until='networkidle')
pdf = await page.pdf(format='A4', print_background=True)
await browser.close()
return pdf
# FastAPI endpoint
from fastapi import FastAPI
from fastapi.responses import Response
app = FastAPI()
@app.get('/pdf')
async def generate_pdf(url: str):
pdf_bytes = await url_to_pdf_async(url)
return Response(content=pdf_bytes, media_type='application/pdf',
headers={'Content-Disposition': 'attachment; filename="document.pdf"'})
Method 4: REST API (No Browser Install)
On serverless functions, Render, Railway, or any host with memory/disk limits, Playwright adds 250MB+ to your image. SnapAPI's PDF endpoint does the same job over HTTP:
import requests, base64, os
def url_to_pdf_api(url: str, output_path: str) -> bytes:
response = requests.post(
'https://api.snapapi.pics/v1/screenshot',
headers={'X-Api-Key': os.environ['SNAPAPI_KEY']},
json={
'url': url,
'format': 'pdf',
'full_page': True,
'wait_for': 'networkidle'
},
timeout=60
)
response.raise_for_status()
pdf_bytes = base64.b64decode(response.json()['pdf'])
with open(output_path, 'wb') as f:
f.write(pdf_bytes)
return pdf_bytes
url_to_pdf_api('https://example.com', 'output.pdf')
Flask PDF Download Endpoint
from flask import Flask, request, send_file, abort
import requests, base64, os, io
app = Flask(__name__)
@app.route('/download-pdf')
def download_pdf():
url = request.args.get('url')
if not url:
abort(400, 'url parameter required')
resp = requests.post(
'https://api.snapapi.pics/v1/screenshot',
headers={'X-Api-Key': os.environ['SNAPAPI_KEY']},
json={'url': url, 'format': 'pdf', 'full_page': True, 'wait_for': 'networkidle'},
timeout=60
)
resp.raise_for_status()
pdf_bytes = base64.b64decode(resp.json()['pdf'])
return send_file(
io.BytesIO(pdf_bytes),
mimetype='application/pdf',
as_attachment=True,
download_name='document.pdf'
)
Method Comparison
| Method | JS support | CSS support | Binary needed | Serverless | Cost |
|---|---|---|---|---|---|
| WeasyPrint | ❌ | Good | ❌ pure Python | ✅ | Free |
| pdfkit + wkhtmltopdf | Partial | Old WebKit | Yes (deprecated) | ❌ | Free |
| Playwright | ✅ full | Excellent | Chromium ~280MB | ⚠️ tricky | Free |
| SnapAPI REST | ✅ full | Excellent | ❌ none | ✅ | $0.0016/call |
pip install requests, get your free key at snapapi.pics. No Chromium. Works on any host.