Python Guide
HTML to PDF in Python
Generate PDFs from HTML in Django, Flask, or FastAPI — without installing WeasyPrint, wkhtmltopdf, or any system-level dependencies. Works in Docker, Lambda, and any Python environment.
Why not WeasyPrint or wkhtmltopdf?
WeasyPrint has incomplete CSS support and breaks on modern layouts. wkhtmltopdf uses a decade-old WebKit build that doesn't support Flexbox or Grid properly. Both require system-level dependencies that complicate Docker builds and break on AWS Lambda entirely.
Papyr runs full Chromium on managed infrastructure. Your Python app makes one HTTP request — no system packages, no Dockerfile changes, no cold-start issues.
Basic usage
Use httpx or requests — no SDK needed.
import httpx # or use requests
def html_to_pdf(html: str) -> bytes:
response = httpx.post(
"https://api.getpapyr.dev/v1/render/pdf",
headers={"Authorization": f"Bearer {os.environ['PAPYR_API_KEY']}"},
json={
"html": html,
"options": {
"format": "A4",
"margin": {"top": "20mm", "right": "20mm",
"bottom": "20mm", "left": "20mm"},
},
},
)
response.raise_for_status()
return response.content
# Usage
pdf_bytes = html_to_pdf("<h1>Hello, PDF!</h1>")
with open("output.pdf", "wb") as f:
f.write(pdf_bytes)Django view
Use Django's template engine to render your HTML, then send it to Papyr.
# views.py
import httpx
from django.http import HttpResponse
from django.template.loader import render_to_string
import os
def invoice_pdf(request, invoice_id):
invoice = Invoice.objects.get(pk=invoice_id)
# Use Django's template engine to render HTML
html = render_to_string("invoices/pdf.html", {"invoice": invoice})
response = httpx.post(
"https://api.getpapyr.dev/v1/render/pdf",
headers={"Authorization": f"Bearer {os.environ['PAPYR_API_KEY']}"},
json={"html": html},
)
response.raise_for_status()
return HttpResponse(
response.content,
content_type="application/pdf",
headers={"Content-Disposition": f'attachment; filename="invoice-{invoice.number}.pdf"'},
)FastAPI endpoint
Use httpx.AsyncClient with async/await for non-blocking PDF generation.
from fastapi import FastAPI
from fastapi.responses import Response
import httpx, os
from jinja2 import Environment, FileSystemLoader
app = FastAPI()
jinja = Environment(loader=FileSystemLoader("templates"))
@app.get("/invoices/{invoice_id}/pdf")
async def get_invoice_pdf(invoice_id: int):
invoice = await db.get_invoice(invoice_id)
template = jinja.get_template("invoice.html")
html = template.render(invoice=invoice)
async with httpx.AsyncClient() as client:
r = await client.post(
"https://api.getpapyr.dev/v1/render/pdf",
headers={"Authorization": f"Bearer {os.environ['PAPYR_API_KEY']}"},
json={"html": html},
)
r.raise_for_status()
return Response(
content=r.content,
media_type="application/pdf",
headers={"Content-Disposition": f'attachment; filename="invoice-{invoice_id}.pdf"'},
)Background tasks with Celery
For reports that take longer or need to be stored, generate asynchronously and upload to S3.
# tasks.py — async PDF generation with Celery
from celery import shared_task
import httpx, boto3, os
@shared_task
def generate_and_store_report(report_id: int):
report = Report.objects.get(pk=report_id)
html = render_to_string("reports/pdf.html", {"report": report})
response = httpx.post(
"https://api.getpapyr.dev/v1/render/pdf",
headers={"Authorization": f"Bearer {os.environ['PAPYR_API_KEY']}"},
json={"html": html, "options": {"landscape": True}},
timeout=30,
)
response.raise_for_status()
s3 = boto3.client("s3")
key = f"reports/{report_id}.pdf"
s3.put_object(Bucket="my-docs", Key=key, Body=response.content,
ContentType="application/pdf")
report.pdf_url = f"https://my-docs.s3.amazonaws.com/{key}"
report.save()Get your API key
Free tier: 100 PDFs/month. No credit card required.