How to Create PDF Files in Python Using pdfkit (HTML to PDF)

Blog / Python Β· August 6, 2023 Β· Updated June 10, 2026 Β· 7 min read
How to Create PDF Files in Python Using pdfkit (HTML to PDF)

To create a PDF file from HTML in Python, install the pdfkit package plus the wkhtmltopdf binary, then call one of pdfkit.from_url(), pdfkit.from_file(), or pdfkit.from_string(). pdfkit is a thin Python wrapper around the wkhtmltopdf command-line tool, which uses a WebKit rendering engine to turn HTML and CSS into a paginated PDF. This makes it a quick way to produce invoices, reports, tickets, and other printable documents straight from your existing HTML templates.

This guide covers installation on Linux, macOS, and Windows, all three generation methods, the options dictionary, how to return a PDF directly from Django and Flask without writing to disk, and an honest 2026 comparison with the alternatives you should consider for new projects.

We have shipped HTML-to-PDF document pipelines like this across Python and Django projects for 12+ years, so the code below reflects what actually works in production, not just the happy path.

Install pdfkit and wkhtmltopdf

pdfkit alone is not enough. It only shells out to the wkhtmltopdf executable, so you need to install both. The single most common error people hit, No wkhtmltopdf executable found, is simply the binary being missing or not on your PATH.

First, install the Python package:

pip install pdfkit

Then install the wkhtmltopdf binary for your operating system:

# Debian / Ubuntu
sudo apt-get update
sudo apt-get install -y wkhtmltopdf

# macOS (Homebrew)
brew install --cask wkhtmltopdf

# Windows: download the installer from https://wkhtmltopdf.org/downloads.html
# Default install path is usually:
#   C:\Program Files\wkhtmltopdf\bin\wkhtmltopdf.exe

Verify the binary is reachable from your shell:

wkhtmltopdf --version
# wkhtmltopdf 0.12.6 (with patched qt)

If wkhtmltopdf is installed but not on your PATH (very common on Windows and on minimal Linux containers), point pdfkit at it explicitly with a configuration object and pass it to every call:

import pdfkit

# Windows example
config = pdfkit.configuration(
    wkhtmltopdf=r"C:\Program Files\wkhtmltopdf\bin\wkhtmltopdf.exe"
)

# Linux / macOS: pass the path that `which wkhtmltopdf` prints
# config = pdfkit.configuration(wkhtmltopdf="/usr/bin/wkhtmltopdf")

pdfkit.from_url("https://micropyramid.com", "out.pdf", configuration=config)

Generate a PDF: from URL, file, or string

pdfkit gives you three entry points. Each takes the source as the first argument and an output path as the second. If you pass False as the output, pdfkit returns the PDF as raw bytes instead of writing a file, which is exactly what you want for a web response (see the Django and Flask sections below).

From a URL

Fetch a live web page and render it to PDF:

import pdfkit

pdfkit.from_url("https://micropyramid.com", "site.pdf")

From an HTML file

Render a local .html file. Use enable-local-file-access (covered below) when the file references local CSS, images, or fonts:

import pdfkit

pdfkit.from_file("invoice.html", "invoice.pdf")

From an HTML string

Render an HTML string you build in memory, for example from a rendered template:

import pdfkit

html = "<h1>Invoice #1042</h1><p>Thank you for your business.</p>"
pdfkit.from_string(html, "invoice.pdf")

Control the output with the options dict

The options dictionary maps directly to wkhtmltopdf command-line flags. The most useful ones set page size, orientation, margins, and DPI. Boolean flags (those that take no value) are passed with an empty string "" as the value.

import pdfkit

options = {
    "page-size": "A4",
    "orientation": "Portrait",
    "margin-top": "0.75in",
    "margin-right": "0.75in",
    "margin-bottom": "0.75in",
    "margin-left": "0.75in",
    "encoding": "UTF-8",
    "dpi": 300,
    "enable-local-file-access": "",  # required for local CSS/images/fonts
}

pdfkit.from_file("invoice.html", "invoice.pdf", options=options)

Why enable-local-file-access matters: newer builds of wkhtmltopdf block file-system access by default for security. Without this flag, a PDF rendered from a local file or string silently drops your stylesheets and images, which is the usual cause of "my CSS and images are missing" reports. Adding "enable-local-file-access": "" (or its older alias "enable-local-file-access": None) restores access to local assets.

For the full list of supported flags, see the wkhtmltopdf documentation.

Return a PDF from Django without writing to disk

In a web app you rarely want a temp file. Render your template to a string, pass False as the output so pdfkit returns bytes, and wrap those bytes in an HttpResponse. This is thread-safe and leaves no files to clean up.

import pdfkit
from django.http import HttpResponse
from django.template.loader import render_to_string


def invoice_pdf(request, invoice_id):
    # render_to_string returns the final HTML for the template
    html = render_to_string("invoice.html", {"invoice_id": invoice_id})

    options = {"page-size": "A4", "enable-local-file-access": ""}
    # output_path=False -> pdfkit returns the PDF as bytes
    pdf_bytes = pdfkit.from_string(html, False, options=options)

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

Use inline instead of attachment in the Content-Disposition header if you want the PDF to open in the browser's viewer rather than download. The modern render_to_string helper replaces the old get_template(...).render(Context(...)) pattern, which has not worked since Django 1.10 removed Context from render().

Return a PDF from Flask

The Flask version is the same idea: render the template, generate bytes, and return a Response with the right content type.

import pdfkit
from flask import Flask, render_template, Response

app = Flask(__name__)


@app.route("/invoice/<int:invoice_id>.pdf")
def invoice_pdf(invoice_id):
    html = render_template("invoice.html", invoice_id=invoice_id)

    options = {"page-size": "A4", "enable-local-file-access": ""}
    pdf_bytes = pdfkit.from_string(html, False, options=options)

    return Response(
        pdf_bytes,
        mimetype="application/pdf",
        headers={
            "Content-Disposition": f'inline; filename="invoice-{invoice_id}.pdf"'
        },
    )

If you build async APIs with FastAPI, the same pdfkit.from_string(html, False) call works inside a route; return the bytes in a Response(content=pdf_bytes, media_type="application/pdf"). Because wkhtmltopdf is a blocking subprocess, run it in a thread pool (for example await run_in_threadpool(...)) so it does not stall the event loop.

Should you still use pdfkit in 2026?

Be aware of an important caveat: wkhtmltopdf is effectively in maintenance / archived status. The upstream project is no longer actively developed, its bundled WebKit engine is years behind modern browsers, and it struggles with current CSS (flexbox, grid) and JavaScript-rendered content. pdfkit still works and is perfectly fine for simple, stable HTML or for maintaining an existing system, but for new projects you should weigh the alternatives below.

Tool Approach CSS / JS fidelity Best for Status
pdfkit + wkhtmltopdf Wraps the wkhtmltopdf CLI (WebKit) Dated WebKit; weak on modern CSS/JS Simple HTML, legacy systems Archived / maintenance
WeasyPrint Pure-Python HTML+CSS to PDF, no browser Strong, standards-based CSS (print-focused) New projects, invoices, reports Actively maintained
Playwright (headless Chromium page.pdf()) Drives a real Chromium browser Best fidelity; handles modern CSS + JS JS-heavy pages, pixel-perfect output Actively maintained
ReportLab Programmatic PDF building (no HTML) N/A (you draw the layout) Precise, data-driven documents Actively maintained

Recommendation: for greenfield work, reach for WeasyPrint when your documents are HTML and CSS (the common invoice/report case), or Playwright when you need a real browser to render JavaScript or complex modern layouts. Keep pdfkit for simple, legacy needs where it already works and you do not want to change the toolchain. Use ReportLab when you would rather build the PDF programmatically than maintain HTML templates.

Frequently Asked Questions

Why do I get "No wkhtmltopdf executable found"?

Because pdfkit is only a wrapper and the wkhtmltopdf binary is missing or not on your PATH. Install the binary for your OS (apt on Debian/Ubuntu, Homebrew cask on macOS, the installer on Windows), confirm it with wkhtmltopdf --version, and if it is installed in a non-standard location pass configuration=pdfkit.configuration(wkhtmltopdf="/full/path/to/wkhtmltopdf") to your call.

Why are my images and CSS missing from the generated PDF?

Newer wkhtmltopdf builds block local file-system access by default for security, so local stylesheets, images, and fonts are silently dropped. Add "enable-local-file-access": "" to your options dict to allow them. For remote assets, use absolute https:// URLs and make sure the server can reach them.

How do I return a PDF from Django or Flask without writing a file to disk?

Pass False as the output path so pdfkit returns the PDF as bytes instead of saving it: pdf_bytes = pdfkit.from_string(html, False). In Django wrap the bytes in HttpResponse(pdf_bytes, content_type="application/pdf"); in Flask return a Response(pdf_bytes, mimetype="application/pdf"). Set the Content-Disposition header to attachment to download or inline to open in the browser.

What is the difference between pdfkit and WeasyPrint?

pdfkit wraps the external wkhtmltopdf CLI, which uses an old WebKit engine and is now in maintenance status. WeasyPrint is a pure-Python library with no external browser dependency and much stronger, standards-based CSS support aimed at print. For new HTML-to-PDF work, WeasyPrint is usually the better choice; pdfkit is best kept for simple or legacy systems.

How do I set page size, margins, and orientation?

Pass an options dictionary whose keys map to wkhtmltopdf flags, for example {"page-size": "A4", "orientation": "Landscape", "margin-top": "0.75in"}. Flags that take no value (boolean flags) are passed with an empty string "" as the value, such as "enable-local-file-access": "".

Why is my JavaScript-rendered content blank in the PDF?

wkhtmltopdf's bundled WebKit engine is years out of date and runs JavaScript poorly, so dynamic, client-rendered content often comes out blank. For JS-heavy pages, render with a real browser using Playwright's headless Chromium and its page.pdf() method, which produces output matching what Chrome displays.

Wrapping up

pdfkit remains a quick, dependable way to turn existing HTML into PDFs in Python, as long as you remember to install the wkhtmltopdf binary, enable local file access for your assets, and return bytes for web responses. For new builds, weigh WeasyPrint or Playwright against it. If you need help designing a reliable document-generation pipeline, MicroPyramid has delivered 50+ projects with HTML-to-PDF, reporting, and templating workflows across Python and Django applications over the last 12+ years.

Share this article