Python Tutorial

Convert Web Page to PDF in Python (2026)

April 202612 min readPlaywright · WeasyPrint · pdfkit · SnapAPI

Four methods for generating PDFs from URLs and HTML in Python — from WeasyPrint (pure Python, no browser) to Playwright (full Chrome rendering) to a REST API (nothing to install). With Django, Flask, and Celery examples.

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
WeasyPrint limitation: No JavaScript execution. If your HTML loads content via XHR/fetch, WeasyPrint won't see it. Use Playwright for JS-rendered content.

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
})
2026 note: wkhtmltopdf is no longer maintained and based on Qt WebKit from 2015. Modern CSS features (CSS Grid, custom properties, modern flexbox) may render incorrectly. Consider WeasyPrint or Playwright instead.

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

MethodJS supportCSS supportBinary neededServerlessCost
WeasyPrintGood❌ pure PythonFree
pdfkit + wkhtmltopdfPartialOld WebKitYes (deprecated)Free
Playwright✅ fullExcellentChromium ~280MB⚠️ trickyFree
SnapAPI REST✅ fullExcellent❌ none$0.0016/call
Python PDF quickstart: pip install requests, get your free key at snapapi.pics. No Chromium. Works on any host.