Tutorial

HTML to PDF Python: 4 Ways to Generate PDFs in 2026 (With Code)

Generating PDFs from HTML in Python has always been more annoying than it should be. There's no built-in solution, the libraries have frustrating limitations, and getting consistent rendering across environments takes real effort.

This guide covers every practical approach in 2026, with working code examples for each. Skip to the verdict if you want the short answer.

Method 1: WeasyPrint

1
WeasyPrint — Pure Python, no browser
Pros
  • Pure Python, no Chromium
  • Good CSS Paged Media support
  • Active maintenance
  • Header/footer via CSS
Cons
  • No JavaScript execution
  • System deps (Pango, Cairo, GDK-PixBuf)
  • Complex CSS sometimes breaks
  • Flexbox support is incomplete
pip install weasyprint
from weasyprint import HTML, CSS

# From HTML string
html_content = """






  

Invoice #1234

April 4, 2026

ItemAmount
SnapAPI Pro Plan$79.00
""" # To file HTML(string=html_content).write_pdf('/tmp/invoice.pdf') # To bytes (for HTTP response) pdf_bytes = HTML(string=html_content).write_pdf() # From URL HTML(url='https://yourapp.com/invoice/123').write_pdf('/tmp/invoice.pdf') # With base URL (resolves relative links correctly) HTML(string=html_content, base_url='https://yourapp.com').write_pdf('/tmp/invoice.pdf')

WeasyPrint is ideal for document-style PDFs — invoices, contracts, reports — where you control the HTML and don't need JavaScript. It implements CSS Paged Media properly, which means @page, page-break-before, and print-specific CSS actually work. Where it struggles: CSS Grid (partial support), JavaScript-rendered content (none), and complex visual designs that depend on modern CSS.

Method 2: Playwright (Recommended for JS-heavy apps)

2
Playwright — Real Chromium, full JS
Pros
  • Real browser — all CSS works
  • Full JavaScript rendering
  • Handles React/Vue/Next.js apps
  • Actively maintained by Microsoft
Cons
  • ~600MB browser download on install
  • 150-200MB memory per browser instance
  • Async only (requires asyncio)
  • Cold start latency (1-3s)
pip install playwright
python -m playwright install chromium
import asyncio
from playwright.async_api import async_playwright

async def html_to_pdf(url: str, output_path: str):
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()

        # Navigate and wait for network to settle
        await page.goto(url, wait_until='networkidle')

        # Also wait for fonts to render
        await page.evaluate("document.fonts.ready")

        pdf_bytes = await page.pdf(
            format='A4',
            print_background=True,
            margin={
                'top': '20mm',
                'bottom': '20mm',
                'left': '15mm',
                'right': '15mm'
            }
        )
        await browser.close()
        return pdf_bytes

# Run it
async def main():
    pdf = await html_to_pdf('https://yourapp.com/invoice/123', '/tmp/invoice.pdf')
    with open('/tmp/invoice.pdf', 'wb') as f:
        f.write(pdf)

asyncio.run(main())

Playwright in a web framework (sync wrapper)

from playwright.sync_api import sync_playwright

def generate_pdf_sync(url: str) -> bytes:
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        page.goto(url, wait_until='networkidle')
        pdf = page.pdf(
            format='A4',
            print_background=True,
            margin={'top': '20mm', 'bottom': '20mm', 'left': '15mm', 'right': '15mm'}
        )
        browser.close()
        return pdf

Playwright is the most reliable self-hosted option for 2026. It handles everything — React apps, custom fonts, CSS Grid, modern animations, lazy-loaded images. The cost is the Chromium binary and memory overhead per request. For low-to-medium volume (under ~500 PDFs/day), the setup cost is worth it.

Method 3: Pyppeteer

3
Pyppeteer — Python port of Puppeteer
Pros
  • Familiar if you know Puppeteer
  • Real Chromium rendering
  • Full JavaScript support
Cons
  • Unofficial port — not maintained by Google
  • Lags behind Puppeteer features
  • Playwright is better in every way
  • Same memory issues as Playwright
pip install pyppeteer
import asyncio
from pyppeteer import launch

async def generate_pdf(url: str) -> bytes:
    browser = await launch(headless=True)
    page = await browser.newPage()
    await page.goto(url, {'waitUntil': 'networkidle0'})
    pdf = await page.pdf({
        'format': 'A4',
        'printBackground': True,
        'margin': {'top': '20mm', 'bottom': '20mm', 'left': '15mm', 'right': '15mm'}
    })
    await browser.close()
    return pdf

pdf_bytes = asyncio.run(generate_pdf('https://yourapp.com/invoice/123'))

Skip pyppeteer in 2026. It's an unofficial Python port of Puppeteer that lags behind, has maintenance gaps, and Playwright exists. If you need browser-based PDF generation in Python, use Playwright instead.

Method 4: PDF API (No Browser on Your Server)

4
PDF API — One HTTP call, zero infra
Pros
  • Zero browser binary on your server
  • Works synchronously — no asyncio required
  • No memory management
  • Scales automatically
Cons
  • Network latency (~200-600ms)
  • URL must be publicly accessible
  • Costs money at volume
pip install httpx  # or requests
import httpx

def generate_pdf_from_url(url: str, api_key: str) -> str:
    """Returns a CDN URL to the generated PDF."""
    response = httpx.post(
        'https://api.snapapi.pics/v1/screenshot',
        headers={'X-Api-Key': api_key},
        json={
            'url': url,
            'format': 'pdf',
            'pdf_format': 'A4',
            'pdf_print_background': True,
            'pdf_margin_top': '20mm',
            'pdf_margin_bottom': '20mm',
            'pdf_margin_left': '15mm',
            'pdf_margin_right': '15mm',
        },
        timeout=30.0
    )
    response.raise_for_status()
    return response.json()['url']  # CDN URL valid for 24h

# Usage
pdf_url = generate_pdf_from_url(
    'https://yourapp.com/invoice/123?token=secure_token',
    api_key='sk_live_your_key'
)
# Redirect user to pdf_url or download it
# Download the PDF bytes if you need them locally
import httpx

def generate_pdf_bytes(url: str, api_key: str) -> bytes:
    api_response = httpx.post(
        'https://api.snapapi.pics/v1/screenshot',
        headers={'X-Api-Key': api_key},
        json={'url': url, 'format': 'pdf', 'pdf_format': 'A4', 'pdf_print_background': True},
        timeout=30.0
    )
    api_response.raise_for_status()
    pdf_url = api_response.json()['url']

    # Download the actual PDF
    pdf_response = httpx.get(pdf_url)
    return pdf_response.content

Django Integration

# views.py — return a PDF as HTTP response
import httpx
from django.http import HttpResponse, Http404
from django.contrib.auth.decorators import login_required

@login_required
def download_invoice_pdf(request, invoice_id):
    try:
        invoice = Invoice.objects.get(id=invoice_id, user=request.user)
    except Invoice.DoesNotExist:
        raise Http404

    # Generate a signed URL to your invoice render view
    signed_url = request.build_absolute_uri(f'/invoices/{invoice_id}/render/?token={invoice.pdf_token}')

    # Call the PDF API
    resp = httpx.post(
        'https://api.snapapi.pics/v1/screenshot',
        headers={'X-Api-Key': 'sk_live_xxx'},
        json={'url': signed_url, 'format': 'pdf', 'pdf_format': 'A4', 'pdf_print_background': True},
        timeout=30
    )
    resp.raise_for_status()
    pdf_url = resp.json()['url']

    # Option A: redirect to CDN URL
    return HttpResponseRedirect(pdf_url)

    # Option B: proxy the bytes
    # pdf_bytes = httpx.get(pdf_url).content
    # return HttpResponse(pdf_bytes, content_type='application/pdf',
    #     headers={'Content-Disposition': f'attachment; filename="invoice-{invoice_id}.pdf"'})

FastAPI Integration

from fastapi import FastAPI, Depends, HTTPException
from fastapi.responses import RedirectResponse
import httpx

app = FastAPI()

@app.get('/invoices/{invoice_id}/pdf')
async def get_invoice_pdf(invoice_id: int, token: str):
    # Validate token against your DB...
    invoice_url = f'https://yourapp.com/invoices/{invoice_id}/render?token={token}'

    async with httpx.AsyncClient() as client:
        resp = await client.post(
            'https://api.snapapi.pics/v1/screenshot',
            headers={'X-Api-Key': 'sk_live_xxx'},
            json={
                'url': invoice_url,
                'format': 'pdf',
                'pdf_format': 'A4',
                'pdf_print_background': True,
            },
            timeout=30.0
        )

    if resp.status_code != 200:
        raise HTTPException(status_code=500, detail='PDF generation failed')

    pdf_url = resp.json()['url']
    return RedirectResponse(url=pdf_url)

Comparison Table

Method JS support Setup Memory Serverless Cost
WeasyPrint None System libs Low Limited Free
Playwright Full npm + Chromium 150-200MB/browser Complex Free + infra
Pyppeteer Full pip + Chromium 150-200MB/browser Complex Free + infra
SnapAPI Full pip install httpx Zero Yes Free tier → $19/mo

Which Method Should You Use?

Static HTML with no JS (invoices, contracts, reports): Start with WeasyPrint. It's pure Python, has no browser overhead, and handles document-style PDFs well. Add CSS Paged Media for headers/footers and page numbers.

React/Vue/Next.js apps or JS-required rendering: Use Playwright. It's the best self-hosted option with full modern browser support. Budget ~200MB memory per concurrent PDF and handle cold starts gracefully.

Serverless (Lambda, Cloud Run, Fly.io) or low-ops preference: Use a PDF API. The Chromium binary is too large for most serverless environments, and the cold start penalty is brutal. One HTTP call from Python is simpler, more reliable, and often cheaper when you factor in compute time.

High volume (1,000+ PDFs/day): Run the numbers. At 50,000 PDFs/month, SnapAPI's Pro tier ($79/mo) is likely cheaper than the compute cost of running Playwright servers, including memory overhead, autoscaling, and engineering time for browser crash handling.

Generate PDFs from Python with one HTTP call

200 free PDFs/month, no credit card. Full JS rendering, modern CSS, works with Django, FastAPI, Flask.

Get your free API key →