How to Convert HTML to PDF in Python with Playwright

Convert HTML to PDF in Python with Playwright. A page.pdf() script for an HTML string or a URL, with A4, margins, and background colors.

A laptop and a short stack of printed pages on a desk, the screen too soft to read

Use the URL to PDF Converter when you want the file without installing anything. The steps below are the detail.

Convert HTML to PDF in Python with Playwright. The script below takes an HTML string or a public URL and writes an A4 file with margins and background colors. page.pdf() uses print CSS unless you switch the page to screen media first.

Install Playwright and Chromium

Python 3 and pip are enough. The second command downloads Chromium, which is the browser that paints the page.

pip install playwright
playwright install chromium

Convert an HTML string

Save this as html_to_pdf.py and run python html_to_pdf.py. The paper size defaults to US Letter, margins default to zero, and backgrounds are left out, so the call sets A4, a margin, and print_background=True.

from playwright.sync_api import sync_playwright

HTML = """
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    body { font-family: sans-serif; }
    h1 { background: #e7f6ee; padding: 12px; }
  </style>
</head>
<body>
  <h1>HTML to PDF in Python</h1>
  <p>This page was rendered by Chromium.</p>
</body>
</html>
"""

with sync_playwright() as playwright:
    browser = playwright.chromium.launch()
    page = browser.new_page()
    page.set_content(HTML)
    page.pdf(
        path="report.pdf",
        format="A4",
        print_background=True,
        margin={"top": "12mm", "right": "12mm", "bottom": "12mm", "left": "12mm"},
    )
    browser.close()

page.pdf() also returns the PDF as bytes when you do not pass path. prefer_css_page_size defaults to false, so a CSS @page size is ignored until you set that option to true.

Convert a public URL

page.goto() waits for the load event unless you say otherwise. wait_until="networkidle" waits until the network goes quiet, and a page that keeps a connection open can sit there until the timeout. Wait for the document, then print.

with sync_playwright() as playwright:
    browser = playwright.chromium.launch()
    page = browser.new_page()
    page.goto("https://example.com", wait_until="domcontentloaded")
    page.pdf(path="example.pdf", format="A4", print_background=True)
    browser.close()

The URL has to be public. This script does not sign in, and it does not open a page that sits behind a login.

Print CSS and screen CSS

Playwright generates the PDF with print CSS. A rule inside @media print applies. A rule that exists only for the screen does not, unless you switch the media type first.

page.emulate_media(media="screen")
page.pdf(path="screen.pdf", format="A4", print_background=True)

Backgrounds still need print_background=True, or print-color-adjust: exact on the element. A table that must not split in half is a stylesheet problem. Print from CSS to PDF keeps a row intact and repeats the header on the next sheet.

Add page numbers

display_header_footer defaults to false. Chrome draws the header and the footer, and it does not see the CSS of your page. Set a font size on the footer, leave a taller bottom margin, and pass an empty header. An empty header template makes Chrome insert the date and the document title.

page.pdf(
    path="numbered.pdf",
    format="A4",
    print_background=True,
    display_header_footer=True,
    header_template="<span></span>",
    footer_template=(
        '<div style="font-size:10px; width:100%; text-align:center;">'
        '<span class="pageNumber"></span> / <span class="totalPages"></span>'
        "</div>"
    ),
    margin={"top": "12mm", "right": "12mm", "bottom": "18mm", "left": "12mm"},
)

The classes Chrome fills in are pageNumber, totalPages, date, title, and url. Chrome does not draw CSS @page margin boxes.

Where the other libraries fit

pdfkit calls wkhtmltopdf. That project is archived, and its WebKit build misses a lot of modern CSS. The notes on a wkhtmltopdf alternative for modern CSS cover that limit. WeasyPrint renders HTML without launching a browser, so it is a different engine from the script above. The same job in Node is HTML to PDF in Node with Puppeteer.

Skip the install

When you do not want Python or Chromium on the machine, paste the HTML or a public URL into the URL to PDF converter. It uses headless Chrome in screen mode, so a rule that lives only inside @media print is skipped, and the paper size comes from the form rather than from @page. The file is deleted after conversion. There is no signup and no watermark.