pdfkit for HTML to PDF conversion in Python: the 2026 guide

28 May 2026

The fastest way to convert HTML to PDF in Python with pdfkit is to pip install pdfkit, install the wkhtmltopdf binary on the same machine, and call pdfkit.from_string(html, "out.pdf"). The catch: wkhtmltopdf has been archived since 2023, so pdfkit is fine for existing projects and quick scripts but not the right starting point for new builds that need modern CSS or a long maintenance horizon.

Key Takeaways

- pdfkit is a thin Python wrapper around the wkhtmltopdf binary. No binary, no PDF, and most install errors trace back to a missing or unfindable wkhtmltopdf.
- Three methods cover the whole API: pdfkit.from_url(), pdfkit.from_file(), pdfkit.from_string(). The string variant is what Django and Flask apps reach for.
- wkhtmltopdf has been archived since January 2023 with two unpatched critical CVEs (CVE-2022-35583 SSRF at CVSS 9.8, CVE-2020-21365 path traversal at CVSS 7.5). Existing pdfkit setups still work; new projects should plan a migration path.
- When you outgrow pdfkit: WeasyPrint is the strongest pure-Python replacement, xhtml2pdf is a lighter ReportLab-based option, and a hosted Chrome engine keeps the same wkhtmltopdf-style options without the archive risk.

pdfkit is the Python library most developers reach for first and regret next month. The API is honestly the simplest in the Python ecosystem (three methods, one binary), and that simplicity is half the reason teams ship it on Friday and wake up to a security audit on Monday.

The other half is that the wkhtmltopdf binary it wraps was archived in January 2023 with two open critical CVEs, and the news hasn't reached every Stack Overflow answer yet.

This guide walks through pdfkit the way it actually works in 2026: every install error fixed with the exact command, the three from_* methods with runnable code, complete Django and Flask endpoints, the wkhtmltopdf archive problem named honestly, and three migration paths when pdfkit stops fitting.

We're not going to tell you the library is dead, because existing pdfkit setups still produce correct PDFs and millions of them. We are going to make sure you can tell the difference between "still works" and "still a good idea."

How to convert HTML to PDF with pdfkit

Three lines, four if you count the install. Install the Python package, install the wkhtmltopdf binary, and pick the input shape that matches what you have.

pip install pdfkit
# Then install the wkhtmltopdf binary. See the next section.
import pdfkit

pdfkit.from_url('https://example.com', 'out.pdf')        # from a live URL
pdfkit.from_file('invoice.html', 'out.pdf')              # from an HTML file on disk
pdfkit.from_string('<h1>Invoice #1041</h1>', 'out.pdf')  # from a Python string

That's the entire API surface. Three methods, same shape. Each returns True on success and raises an exception on failure. Pass False as the second argument instead of a filename to get the PDF bytes back in memory, which is what you want from a web handler.

Tip: Want the full Python HTML-to-PDF landscape before you commit to pdfkit? Our Python HTML-to-PDF pillar guide covers the full library lineup. Otherwise, read on for the pdfkit walkthrough.

Install wkhtmltopdf for pdfkit (and fix "No wkhtmltopdf executable found")

pdfkit is a Python wrapper around the wkhtmltopdf command-line binary. No binary, no PDF. The error you'll hit first if you skip this step is OSError: No wkhtmltopdf executable found. Here's how to install it on every platform you'll actually deploy to.

Ubuntu / Debian

sudo apt-get install -y wkhtmltopdf

The version in the default repositories has reduced functionality, because it's compiled without the QT patches that wkhtmltopdf depends on for full CSS support. For real production use, grab the patched binary from the wkhtmltopdf releases archive instead. Distro packages produce subtle CSS bugs that are hard to triage later.

macOS

brew install --cask wkhtmltopdf

The Homebrew cask was disabled in December 2024 in response to the archive status. The direct-download installer from wkhtmltopdf.org is the reliable path now. If your local machine has the cask installed from before, it'll keep working, but new contributors will hit the disabled-formula error on brew install.

Windows

choco install wkhtmltopdf

Or use the installer from the official site. After install, confirm wkhtmltopdf.exe is on your PATH. If it isn't, the explicit-configuration fix below is your friend.

Docker

FROM python:3.12-slim
RUN apt-get update && apt-get install -y wkhtmltopdf && rm -rf /var/lib/apt/lists/*
RUN pip install pdfkit

Same QT-patches caveat as Ubuntu. For container images that produce real-quality PDFs, bake in the patched binary from the release archive rather than the distro package.

Fix: "OSError: No wkhtmltopdf executable found"

The binary is installed; pdfkit can't find it on PATH. Two paths to the fix.

Path 1: add the install directory to PATH. On Linux it's usually /usr/local/bin/; on Windows, C:\Program Files\wkhtmltopdf\bin. Restart the shell or the application server after.

Path 2: point pdfkit at the binary explicitly. This is the reliable fix on platforms-as-a-service (Heroku, Render, App Engine) where you don't fully control PATH.

import pdfkit

path = '/usr/local/bin/wkhtmltopdf'  # adjust to your install
config = pdfkit.configuration(wkhtmltopdf=path)
pdfkit.from_string(html, 'out.pdf', configuration=config)

Nadia shipped a Django app to Heroku in early 2026 and lost an afternoon to this error. The buildpack she was using installed wkhtmltopdf in /app/.heroku/bin, which wasn't in the worker's PATH. Passing pdfkit.configuration(wkhtmltopdf='/app/.heroku/bin/wkhtmltopdf') fixed it in the time it took to redeploy.

Fix: "QXcbConnection: Could not connect to display" on Linux

Headless Linux servers don't have an X display, and the older wkhtmltopdf binary uses QT in a way that tries to connect to one. Two fixes. Install the wkhtmltox package (which includes the QT-patched binary), or run wkhtmltopdf under xvfb-run as a fallback. The patched binary is the right answer for production; xvfb-run is a fine band-aid for a CI job.

pdfkit walkthrough: from_url, from_file, from_string, and options

The three methods cover three input shapes. They share the same options dictionary and the same return contract.

From a live URL

import pdfkit

pdfkit.from_url('https://your-app.example.com/invoice/1041', 'out.pdf')

wkhtmltopdf fetches the URL with its own HTTP client (no JavaScript). If the page depends on client-side rendering, you'll get a PDF of the empty skeleton. If the page uses webfonts from a CDN, those may or may not load in time. Self-host fonts or inline them when the PDF has to look right.

From an HTML file

pdfkit.from_file('templates/invoice.html', 'out.pdf')

Best for batch jobs that pre-render templates to files. If you're rendering a Jinja or Django template, skip the disk and go straight to from_string.

From an HTML string

html = '<h1>Invoice #1041</h1><p>Total: $49.00</p>'
pdf_bytes = pdfkit.from_string(html, False)  # False returns bytes

This is the variant web handlers use. Pass False as the second argument and you get a bytes object back, ready for HttpResponse or send_file. Pass a filename to write to disk instead.

Pass options (page size, margins, encoding)

options = {
    'page-size': 'A4',
    'margin-top': '20mm',
    'margin-bottom': '20mm',
    'encoding': 'UTF-8',
    'no-outline': None,
}
pdfkit.from_string(html, 'out.pdf', options=options)

The option names mirror wkhtmltopdf's CLI flags. None as the value renders the flag without an argument, which is how --no-outline translates. The full option set lives in the wkhtmltopdf documentation; pdfkit forwards them verbatim.

Gotcha: tables splitting across pages

If your invoice rows break in the middle of a page, the fix is CSS, not a pdfkit option. Add page-break-inside: avoid to the rows you don't want broken:

tr { page-break-inside: avoid; }
.invoice-totals { page-break-inside: avoid; }

Use the older page-break-inside property. The newer break-inside was added later and wkhtmltopdf's archived WebKit doesn't reliably honor it.

Gotcha: webfonts and late-loading assets

wkhtmltopdf fetches images, stylesheets, and webfonts at render time. A flaky CDN or a slow font host means your PDF can render before the font lands, producing output in Times New Roman or a default sans-serif. Self-host the fonts in your application, inline them as base64 in CSS, or pre-warm the renderer with a --javascript-delay option if the page depends on a settled load.

Use pdfkit in Django (a complete endpoint)

The canonical Django pattern: render a template to a string with render_to_string, hand the string to pdfkit.from_string returning bytes, and return an HttpResponse with application/pdf.

# views.py
from django.http import HttpResponse
from django.template.loader import render_to_string
import pdfkit

def invoice_pdf(request, invoice_id):
    invoice = get_invoice(invoice_id)
    html = render_to_string('invoices/show.html', {'invoice': invoice})
    pdf_bytes = pdfkit.from_string(html, False)

    response = HttpResponse(pdf_bytes, content_type='application/pdf')
    response['Content-Disposition'] = f'attachment; filename="invoice-{invoice_id}.pdf"'
    return response

The template at templates/invoices/show.html is a normal Django template. Whatever HTML and CSS the customer-facing UI already renders, you reuse here.

There's a production gotcha worth catching now. PDF rendering is CPU-bound and blocks the worker that handles it. A "Download PDF" button is fine. A "Email this report to 5,000 customers" loop will starve every other endpoint in your Django app. Push the render onto a Celery task with @app.task, store the bytes to S3, and email the customer a download link or hit a webhook when it lands.

Use pdfkit in Flask (a complete endpoint)

Same pattern, Flask shape: render_template to a string, pipe through pdfkit.from_string, return with send_file wrapped around a BytesIO.

from flask import Flask, render_template, send_file
import pdfkit
from io import BytesIO

app = Flask(__name__)

@app.route('/invoice/<int:invoice_id>.pdf')
def invoice_pdf(invoice_id):
    html = render_template('invoice.html', invoice=get_invoice(invoice_id))
    pdf_bytes = pdfkit.from_string(html, False)
    return send_file(
        BytesIO(pdf_bytes),
        mimetype='application/pdf',
        as_attachment=True,
        download_name=f'invoice-{invoice_id}.pdf'
    )

The Flask version is shorter because send_file does the header work that you write manually in Django. Same scaling caveat applies: render heavy batches on RQ or Celery, not on the request thread.

The pdfkit problem: wkhtmltopdf is archived

Two paragraphs of honest context before the migration paths. The wkhtmltopdf/wkhtmltopdf GitHub repository was archived in January 2023. The wider organization was archived in July 2024. The last released binary was tagged in 2020. None of this stops your existing render from running; the binary doesn't change, your PDFs come out the same. What does change is the maintenance horizon. Modern CSS features land in browser engines every few months. The wkhtmltopdf binary is frozen on an old WebKit that will never see grid, container queries, or the latest flex tweaks.

Two unpatched critical CVEs sit on the binary: CVE-2022-35583 (SSRF, CVSS 9.8) and CVE-2020-21365 (path traversal, CVSS 7.5). They will not be patched, because the project will not ship new releases. If your pdfkit setup renders HTML you control, the practical impact is low. If you render user-supplied URLs (custom domains in PDF generation, dynamic image sources, anything where an attacker can influence the input), the SSRF risk is real.

Beto inherited a pdfkit setup at a fintech in 2023 that rendered customer statements from internal templates. Through 2025 it shipped invoices without incident. The 2026 security audit flagged the CVEs against the wkhtmltopdf version in the container, gave him 30 days to remediate, and he had three options on the table. He picked the hosted-engine option because the wkhtmltopdf-style options translated directly and the migration was a one-day code change.

When pdfkit is still the right call

Stable, server-rendered, CSS-only templates you control. Internal tooling that doesn't render user-supplied URLs. Migration deadlines six or more months out where the cost of switching exceeds the cost of waiting.

When to switch

Anywhere user-supplied URLs feed into a render. Anywhere your designer wants modern CSS features. Anywhere the HTML depends on JavaScript. Anywhere a security audit is on the calendar.

pdfkit alternatives: WeasyPrint, xhtml2pdf, and a hosted engine

Three concrete migration paths. Pick by which constraint moved.

Path CSS support JavaScript Binary required Maintenance Best for
pdfkit + wkhtmltopdf CSS 2.1 + a few CSS 3 Limited (old WebKit) wkhtmltopdf binary Archived 2023 Existing setups, simple internal templates
WeasyPrint CSS 2.1 + most of CSS 3 (grid, flex) No None (pure Python) Active New pdfkit replacements for modern CSS
xhtml2pdf CSS 2.1 subset No None (pure Python) Active (slow) ReportLab-style programmatic PDFs
Hosted Chrome engine Everything Chrome ships Yes None (HTTP only) Vendor-maintained JavaScript HTML, modern CSS, serverless
Hosted wkhtmltopdf engine CSS 2.1 + some CSS 3 Limited None (HTTP only) Vendor-maintained Migrating off local pdfkit with the same options

WeasyPrint (pure-Python, modern CSS, no binary)

from weasyprint import HTML

HTML(string=html).write_pdf('out.pdf')

WeasyPrint is the strongest general-purpose pdfkit replacement. Pure Python, no system binary, much better CSS coverage (flex, grid, modern font properties, paged-media features wkhtmltopdf never shipped). Tradeoffs: heavier dependency footprint, no JavaScript execution, slower per render. For most pdfkit users moving for CSS reasons, WeasyPrint is the answer.

xhtml2pdf (ReportLab-based, lighter)

from xhtml2pdf import pisa

with open('out.pdf', 'wb') as f:
    pisa.CreatePDF(html, dest=f)

xhtml2pdf wraps ReportLab and is the right pick when you need ReportLab's programmatic post-processing (custom layouts, watermarks, form fields) alongside HTML rendering. CSS coverage is weaker than WeasyPrint, no JavaScript.

A hosted Chrome engine (when neither library fits)

import os
import requests

response = requests.post(
    'https://api.transformy.io/v1/render',
    headers={'Authorization': f'Bearer {os.environ["TRANSFORMY_API_KEY"]}'},
    json={
        'html': html,
        'engine': 'chrome',
        'wait_for': '.invoice-total',
        'margin': {'top': '20mm', 'bottom': '20mm'},
    },
)
pdf_url = response.json()['url']

When the HTML needs JavaScript, modern CSS, or you'd rather not maintain a binary at all, a hosted Chrome engine is the path of least surprise. Two options matter for migrating pdfkit users. engine: "chrome" gives you real Chromium rendering with flex, grid, JS, and webfonts. engine: "wkhtmltopdf" keeps the same option names you've been using, which makes the port mostly a requests.post wrapper around your existing options dict. Our wkhtmltopdf option-name mapping guide covers the parameter translation if you're moving an existing pdfkit config.

Hannah ran a solo Lambda service that generated transaction receipts. She spent two weekends fighting wkhtmltopdf Lambda layers (binary too big for the base layer, broken QT on the Lambda Linux variant, cold-start times in the 8-second range) before she switched to the hosted engine. The Python code was a requests.post and a few options. Cold starts dropped to her function's normal warmup time.

Tip: Want to compare your existing pdfkit output to a hosted Chrome render side by side? Render the same HTML on the Transformy free tier and diff the two PDFs before committing to either path.

FAQ: pdfkit in Python

Is pdfkit deprecated?

pdfkit itself, the Python package, is not officially deprecated. The library it wraps, wkhtmltopdf, was archived in January 2023, with no new releases since 2020. In practice that means pdfkit is "still works, no future improvements." Existing setups are fine; new projects should weigh that against active alternatives like WeasyPrint.

Why does pdfkit say "No wkhtmltopdf executable found"?

pdfkit needs the wkhtmltopdf binary installed and reachable. The error means the binary isn't installed, or it is installed but not on the Python process's PATH. The reliable fix is pdfkit.configuration(wkhtmltopdf='/full/path/to/wkhtmltopdf') and pass it as the configuration= argument on each call.

Can pdfkit render JavaScript pages?

Not in a way you should rely on. wkhtmltopdf has a --enable-javascript option, but it runs in an old WebKit that doesn't support modern JS APIs and offers no reliable hook to wait for client-side rendering to settle. If your HTML depends on JavaScript, use a headless Chrome path (a hosted engine, or self-hosted Playwright).

Is pdfkit safe to use in production in 2026?

For HTML you control and templates you own, yes. The two open critical CVEs are SSRF and path traversal in wkhtmltopdf itself; both require attacker-controlled input. If your pdfkit setup renders user-supplied URLs or HTML fragments, you have real risk surface and should plan a migration. If you render trusted, server-side templates only, the practical risk is low.

What's the best pdfkit alternative for Python?

WeasyPrint, for most use cases. It's pure Python, no binary dependency, much stronger modern CSS support, and actively maintained. xhtml2pdf is the lighter ReportLab-based option for ReportLab-style programmatic PDF needs. A hosted Chrome engine is the answer when JavaScript, modern CSS, or operational simplicity matter more than running everything locally.

How do I use pdfkit in Django?

Render your Django template to a string with render_to_string, call pdfkit.from_string(html, False) to get bytes, and return them in an HttpResponse with content_type='application/pdf'. The full code is in the Django section above. For batch renders, push the work onto a Celery task so the request thread stays free.

Video walkthrough: (TODO: editor to paste a verified YouTube embed URL. Recommended search terms: "pdfkit Django HTML to PDF tutorial" or "Python wkhtmltopdf install guide.")

Conclusion: keep pdfkit if it fits, migrate when it doesn't

pdfkit is the simplest Python API for HTML to PDF, and 2026's wkhtmltopdf archive doesn't change that. What it changes is the planning horizon. Use pdfkit for stable templates you own and CSS that doesn't push past 2.1. Plan a migration the moment one of three things bites: user-supplied URLs in the renderer, modern CSS in the templates, or a security audit on the calendar.

Three honest migration paths cover most cases. WeasyPrint is the pure-Python answer. xhtml2pdf is the lighter ReportLab-based option. A hosted Chrome engine lets you keep wkhtmltopdf-style options without owning the archive risk. If the rest of your team also needs help picking among the Python options, our Python HTML-to-PDF pillar guide covers the full landscape.

If you're testing the hosted route, render your first PDF on the Transformy free tier. Point it at the same HTML your Python app already produces, see the output, decide later.