Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use Playwright when your HTML depends on JavaScript. Put the HTML in a browser page with page.set_content(), inject the JavaScript string with page.add_script_tag(content=js), wait for an application-specific ready signal when rendering is asynchronous, and then call page.pdf(). This gives the script a real DOM and browser runtime before the PDF is generated.

Use WeasyPrint instead when the document is already rendered or uses only HTML and CSS. It can convert an HTML string directly, but it is not a browser JavaScript runtime.

Playwright: inject a JavaScript string and write the PDF

Install Playwright and its browser binaries in the environment that will run the conversion:

python -m pip install playwright
python -m playwright install chromium

The following complete example creates an in-memory HTML document, injects JavaScript from a Python string, and writes an A4 PDF:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from playwright.sync_api import sync_playwright

html = """<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Invoice</title>
    <style>
      body { font-family: Arial, sans-serif; margin: 32px; }
      h1 { color: #17324d; }
    </style>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>"""

js = """
document.querySelector('#app').innerHTML = `
  <h1>Rendered before PDF</h1>
  <p>This content was inserted by JavaScript.</p>
`;
"""

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.set_content(html)
    page.add_script_tag(content=js)
    page.pdf(path="output.pdf", format="A4", print_background=True)
    browser.close()

add_script_tag adds a script element to the page. Its content argument accepts raw JavaScript, so the value can come from a database, template, configuration file, or another Python variable. The script runs in the page’s browser context and can use document, DOM events, browser APIs, and the elements created by your HTML.

Use screen styles instead of print styles

PDF generation uses print CSS by default. If your layout was designed for the screen, select screen media before generating the file:

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

Keep the default print media for a print-specific stylesheet. Use print_background=True when background colors and images are part of the design.

Inject an ES module

If the string contains module syntax such as import or export, pass type="module":

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
page.add_script_tag(content=js, type="module")

Module imports still need resolvable URLs and an execution environment that can reach those dependencies.

Wait for asynchronous JavaScript before capturing

Calling page.pdf() immediately after injection can capture an empty or incomplete application when the script fetches data, waits for a component, or performs asynchronous rendering. Make readiness explicit rather than relying on an arbitrary sleep.

from playwright.sync_api import sync_playwright

html = """<!doctype html>
<html><body><div id="app">Loading…</div></body></html>"""

js = """
(async () => {
  const data = await fetch('/data.json').then(response => {
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  });
  document.querySelector('#app').textContent = data.title;
  document.body.dataset.rendered = 'true';
})();
"""

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.set_content(html)
    page.add_script_tag(content=js)
    page.wait_for_function("document.body.dataset.rendered === 'true'")
    page.pdf(path="output.pdf", format="A4")
    browser.close()

page.evaluate() is another way to run code in the page. Playwright waits automatically when the evaluated result is asynchronous or a Promise, but you should still expose a clear DOM marker, selector, or other readiness condition for the PDF step.

Useful readiness signals

  • Set document.body.dataset.rendered = 'true' after all data and components are present, then use page.wait_for_function().
  • Render a final element and wait with page.wait_for_selector('#report-complete') .
  • For a known, short animation or delayed widget, use a deliberate timeout only when no stronger signal is available.
  • Handle failures in the page script so the marker is not set when required data failed to load.

HTML strings, URLs, and relative assets

page.set_content(html) creates a document from the string. Relative images, stylesheets, fonts, and fetch URLs need a meaningful origin or absolute URLs. If the page relies on site-relative resources, navigate to an appropriate page first or change those references to absolute URLs. A string document has no normal file location from which a relative path can be inferred.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For local resources, serve the assets over HTTP or use a carefully chosen local URL that the browser can access. A browser may also block requests because of cross-origin rules, authentication, certificate errors, or unavailable network access. Diagnose the page as a browser page rather than assuming PDF generation itself is broken.

WeasyPrint when JavaScript is not required

WeasyPrint accepts an HTML string through HTML(string=...). When that string contains relative images or stylesheets, supply base_url so the resources can be resolved:

from weasyprint import HTML

html = """<!doctype html>
<html><body><h1>Static report</h1></body></html>"""

pdf_bytes = HTML(
    string=html,
    base_url="/srv/app/templates"
).write_pdf()

with open("output.pdf", "wb") as output:
    output.write(pdf_bytes)

Calling write_pdf() without a target returns PDF bytes. You can return those bytes from a web endpoint, store them in object storage, or write them to a file. WeasyPrint is appropriate for already-rendered HTML and CSS workflows; it does not execute browser JavaScript. If a script must build the DOM, fetch data, measure layout, or use browser APIs, render with Playwright first.

Choosing the right engine

Requirement Playwright WeasyPrint
Execute JavaScript Yes, in a browser page No browser JavaScript runtime
Python string input page.set_content(html), then inject or evaluate code HTML(string=html)
PDF output page.pdf(), with print or screen media control write_pdf() returns bytes or writes to a target
Relative assets Use a meaningful page origin or absolute resource URLs Pass base_url for string HTML

The practical decision is simple: if JavaScript changes what must appear in the PDF, use Playwright and wait for that change. If the input is static HTML/CSS, WeasyPrint avoids the overhead of a browser.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Production checklist

  • Pin and install the browser: deploy the Playwright package and the Chromium binary together, and verify that the runtime user can launch it.
  • Set an explicit page size: choose format="A4" or another supported format instead of relying on a default.
  • Control media: select screen only when screen CSS is intended; otherwise retain print media.
  • Wait for readiness: use a selector, marker, or Promise-based condition tied to your application.
  • Make data failures visible: check response status and surface errors instead of producing a plausible but incomplete PDF.
  • Close resources: close each browser after the PDF operation, including error paths in a larger service.
  • Keep scripts deterministic: disable unneeded animation, avoid random content, and use fixed test data when PDF output must be reproducible.
  • Protect secrets: do not embed private credentials in HTML or JavaScript that can be saved with a generated document.

Troubleshooting common failures

The PDF contains the loading screen

The capture happened before asynchronous code completed. Add a readiness marker after the final DOM update and wait for it with page.wait_for_function() or page.wait_for_selector(). Do not replace a missing signal with a long fixed sleep unless the application truly has no observable completion state.

The JavaScript string does nothing

Check that the selector exists, that the injected string is valid JavaScript, and that the script is added after page.set_content(). Capture browser console messages and page errors while debugging. If the code uses import, inject it as a module.

Images or CSS are missing

Relative URLs cannot resolve without a usable origin. Prefer absolute URLs in browser-rendered HTML, or provide a page context that can resolve the resources. For WeasyPrint, pass the filesystem or URL base through base_url.

The design looks different from the website

page.pdf() uses print media by default. Call page.emulate_media(media="screen") for screen styles, and include print_background=True when backgrounds are required. Also account for the viewport, fonts, device scale, and responsive breakpoints.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The browser will not launch

Install the matching Playwright browser binary, check executable permissions and system dependencies, and run the process under the same user as production. In containers, use a supported base image or install the required Chromium libraries.

A fetch works in development but not in production

Inspect the browser’s network and console errors. The production runtime may lack DNS or outbound access, reject a certificate, require authentication, or hit cross-origin restrictions. Make the resource available to the page and handle non-OK responses explicitly.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If you need a screenshot or PDF of a URL rather than a Python-controlled HTML string, ScreenshotNeo provides a single HTTP request. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed as clean shots, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

See the ScreenshotNeo documentation for all options, including PDF paper size, margins, landscape mode, page ranges, custom JavaScript and CSS, waits, selectors, cookies, headers, user agents, geolocation, caching, signed links, asynchronous jobs, bulk capture, and HTML/CSS-to-image conversion.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

The free plan includes 1,000 screenshots per month without a card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.

Frequently Asked Questions

Can I inject JavaScript before calling page.set_content() in Playwright?

No. Set the document content first so the injected script can find and modify the intended DOM. Add the script after page.set_content(html).

Should I use a fixed delay or wait_for_function()?

Prefer an application-specific selector, marker, or Promise-backed condition. A fixed delay is less reliable because network and rendering time vary.

Does WeasyPrint run inline script tags?

No. Choose Playwright when JavaScript must execute; use WeasyPrint for static or already-rendered HTML and CSS.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What does page.pdf() return?

The call writes the PDF to the path supplied through path. WeasyPrint’s write_pdf() can instead return PDF bytes when no target is supplied.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.