Free tools Windows power users keep installed

One-click scans. No signup required.

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

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

With WeasyPrint, keep both your HTML and stylesheet in memory: create the document with HTML(string=html_text), create the stylesheet with CSS(string=css_text), then pass that stylesheet to write_pdf(stylesheets=[stylesheet]). The following minimal program writes a styled PDF without creating a temporary CSS file.

from weasyprint import CSS, HTML

html = HTML(string="""
    <h1>Report</h1>
    <p>Generated from strings.</p>
""")
stylesheet = CSS(string="""
    @page { size: A4; margin: 2cm }
    h1 { color: #174a7e }
""")
html.write_pdf("report.pdf", stylesheets=[stylesheet])

CSS(string=...) is the important distinction: without the named string argument, a value may be interpreted as a filename or URL rather than stylesheet text. This article uses WeasyPrint because its documented API directly supports in-memory CSS and HTML. See the WeasyPrint first-steps documentation for the current API details.

Install WeasyPrint and prepare the inputs

Install the package in the Python environment that will generate the PDF:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
python -m pip install weasyprint

WeasyPrint also depends on native libraries. Follow the installation instructions for your operating system if the import fails because a platform library is missing. Keep the HTML and CSS as Unicode strings; Python triple-quoted strings make multiline templates readable.

The smallest working pattern

  1. Build an HTML string.
  2. Build a CSS string.
  3. Construct HTML(string=...) and CSS(string=...).
  4. Pass the CSS object in the stylesheets list to write_pdf().

The list can contain one stylesheet or several. Later stylesheets participate in the normal cascade, so put overrides after base rules.

Generate a PDF entirely in memory

Omit the output argument to receive PDF bytes. This is useful for an HTTP response, object storage upload, or a database blob.

from weasyprint import CSS, HTML

html_text = """
<!doctype html>
<html>
  <head><meta charset="utf-8"></head>
  <body>
    <h1>Quarterly report</h1>
    <p class="lede">This document was rendered from Python strings.</p>
    <table>
      <tr><th>Item</th><th>Amount</th></tr>
      <tr><td>Service</td><td>$120</td></tr>
    </table>
  </body>
</html>
"""

css_text = """
@page { size: A4; margin: 18mm 16mm; }
body { font-family: sans-serif; color: #20252b; }
h1 { color: #174a7e; margin-bottom: 4mm; }
.lede { color: #56616d; }
table { width: 100%; border-collapse: collapse; margin-top: 8mm; }
th, td { border: 0.2mm solid #b8c2cc; padding: 2mm; text-align: left; }
th { background: #e9f0f7; }
"""

document = HTML(string=html_text)
stylesheet = CSS(string=css_text)
pdf_bytes = document.write_pdf()

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

Use binary mode ("wb") because a PDF is a byte stream, not text. In a web framework, return pdf_bytes with a PDF content type instead of writing it to disk.

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

Make dynamic CSS safe and predictable

Inject values deliberately

If colors, margins, or branding are generated at runtime, validate them before interpolating them into CSS. A controlled template is easier to audit than concatenating arbitrary declarations:

from weasyprint import CSS, HTML

brand_color = "#0b6e4f"       # validate against an allow-list or a color parser
margin = "18mm"               # validate units and range

css_text = f"""
@page {{ size: A4; margin: {margin}; }}
h1 {{ color: {brand_color}; }}
"""

pdf = HTML(string="<h1>Invoice</h1>").write_pdf(
    stylesheets=[CSS(string=css_text)]
)

Never treat untrusted user input as raw CSS. Restrict accepted values (for example, a known set of hex colors and numeric dimensions) before formatting the string.

Use multiple in-memory stylesheets

base = CSS(string="body { font-size: 10pt; }")
brand = CSS(string="h1 { color: #174a7e; }")
HTML(string=html_text).write_pdf("report.pdf", stylesheets=[base, brand])

This is useful when a common stylesheet is combined with a per-customer theme. Keep the order explicit so an override does not accidentally lose to an earlier rule.

Control page size, margins, and page breaks

Print-specific rules belong in the same CSS string. The @page rule controls paper dimensions and margins; page-break properties can keep headings or table rows together. WeasyPrint broadly supports CSS 2.1, but it documents exceptions and additional limitations, so verify every property your layout depends on in its API and feature reference.

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.
css_text = """
@page { size: Letter; margin: 0.7in; }
@page :first { margin-top: 0.4in; }
.report-section { break-inside: avoid; }
h2 { break-after: avoid; }
"""

Browser CSS is not automatically PDF CSS. A property that works in Chrome may be unsupported or behave differently in WeasyPrint; consult the feature reference rather than assuming full HTML5/CSS coverage.

Fonts, images, and relative URLs

Register custom fonts with FontConfiguration

When CSS contains @font-face, the documented pattern creates one FontConfiguration and passes it both to CSS and to write_pdf().

from weasyprint import CSS, HTML
from weasyprint.text.fonts import FontConfiguration

font_config = FontConfiguration()
css = CSS(
    string="""
    @font-face {
      font-family: "Report Sans";
      src: url("fonts/report-sans.woff2");
    }
    body { font-family: "Report Sans", sans-serif; }
    """,
    base_url="/srv/my-report",
    font_config=font_config,
)

HTML(string="<p>Custom type</p>", base_url="/srv/my-report").write_pdf(
    "report.pdf",
    stylesheets=[css],
    font_config=font_config,
)

Use the same configuration for the document’s CSS and PDF write step. The first-steps guide shows this arrangement and explains resource loading.

Give relative resources a base location

Relative URLs such as images/logo.png and fonts/report-sans.woff2 need a meaningful base URL. Set base_url on HTML and, when the stylesheet itself references files, on CSS. WeasyPrint’s default fetcher can open local files and HTTP URLs, but its default HTTP client does not provide advanced cookie or authentication handling. Protected resources therefore require a suitable custom fetcher or an accessible, authenticated asset pipeline. These behaviors and security considerations are described in the resource-fetching documentation.

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

Data URLs and embedded assets

For small generated images, embedding a data URL avoids a relative-path problem. For large assets, a stable local directory or HTTP origin with an explicit base_url is easier to maintain and keeps the HTML string smaller.

Return, cache, and stream the result

Rendering is CPU- and memory-intensive compared with assembling plain text. Reuse the HTML and CSS templates, avoid repeatedly downloading identical assets, and generate only the pages needed for a request. If you need a file, pass its path directly to write_pdf("report.pdf"); if you need an API response, call write_pdf() once and send the returned bytes. Do not convert PDF bytes to a text string.

For concurrent services, place rendering in a worker process or queue and enforce request timeouts around external resources. The renderer’s output still depends on supported CSS, available fonts, and successful resource fetches; valid source markup alone does not guarantee the intended visual result. WeasyPrint’s common use cases notes these practical limits.

Troubleshoot the common failures

“My CSS text is treated as a file”

Cause: constructing CSS(css_text) positionally or using the wrong keyword. Fix: use CSS(string=css_text). Likewise, use HTML(string=html_text) for in-memory markup.

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

“The PDF is unstyled”

Cause: the stylesheet object was created but not supplied to write_pdf(). Fix:

HTML(string=html_text).write_pdf(
    "report.pdf",
    stylesheets=[CSS(string=css_text)],
)

Also check selector spelling and CSS syntax. A malformed declaration can be ignored while the rest of the document renders.

“Images or fonts are missing”

Cause: relative URLs have no base location, or the default fetcher cannot authenticate to the resource. Fix: set base_url, use absolute accessible URLs, embed small assets, or implement a custom fetcher for protected content. Inspect the path and permissions from the same environment that runs Python.

“The font does not appear”

Cause: missing FontConfiguration, an invalid font URL, or a format the environment cannot read. Fix: create one FontConfiguration, pass it to both CSS and write_pdf(), and verify the font file is reachable.

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

“A browser layout does not match”

Cause: WeasyPrint is not a full browser engine and documents CSS 2.1 exceptions and other feature limits. Check the feature reference, replace unsupported properties with print-oriented rules, and test the exact WeasyPrint version deployed.

“The request hangs while rendering”

Cause: a remote image, font, or stylesheet is slow or unreachable. Fix: make dependencies local or reliably cacheable, set application-level time limits, and avoid blocking rendering on optional third-party assets.

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

When another Python renderer is a better fit

Tool What the documented material establishes CSS-string implication
xhtml2pdf HTML-to-PDF conversion built with ReportLab, html5lib, and pypdf; documentation describes HTML5, CSS 2.1, and some CSS 3 support. The quickstart accepts HTML through pisa.CreatePDF() and a file-like object, but the available material does not establish an identical standalone CSS(string=...) API. Check its CSS reference and API before porting this pattern.
fpdf2 The manual states that neither all HTML5 nor CSS is supported and points readers to WeasyPrint and xhtml2pdf for more robust HTML-to-PDF conversion. Choose it for its own document API, not when the requirement is applying a broad HTML stylesheet.
ReportLab The user guide documents its PDF-generation library and commercial offerings. It is a different, programmatic layout approach rather than evidence of a WeasyPrint-compatible CSS-string interface.

Compare the CSS properties your design needs, relative-resource and authentication behavior, font handling, and input/output forms. The available documentation does not establish a performance winner among these tools.

Or skip the browser setup

If the page you need is already reachable at a URL, ScreenshotNeo can capture it as a PNG, JPEG, WebP, or PDF through one GET request. It is a website screenshot API, not a replacement for WeasyPrint’s in-memory CSS object: use it when your HTML/CSS is deployed as a page and you want a rendered capture.

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

Its cleanup steps accept cookie or consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and whether the request was billed. An MCP server supplies take_screenshot, get_page_info, and capture_pdf tools to Claude, Cursor, and other MCP clients.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js

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 ScreenshotNeo documentation lists options such as PDF paper size, margins, landscape mode and page ranges, plus custom CSS and JavaScript, waits, selectors, headers, cookies, device presets, caching, signed links, asynchronous jobs, webhooks, bulk capture, and a usage API. Every feature is on every plan: 1,000 shots per month are free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Frequently Asked Questions

Can I pass a CSS filename and a CSS string together?

Yes. Create each stylesheet with the appropriate constructor (for example, a file-backed CSS object and CSS(string=dynamic_rules)) and include both objects in the stylesheets list, ordering the dynamic override where you need it in the cascade.

How do I produce PDF bytes without writing a temporary file?

Call HTML(…).write_pdf() without an output argument. The returned value is bytes that you can return from an HTTP handler or upload directly.

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

Why does a relative URL work in HTML but fail in CSS?

The stylesheet may have a different reference location. Supply an explicit base_url when constructing CSS, and supply one for HTML when its markup also contains relative resources.

Is WeasyPrint interchangeable with a browser for every CSS property?

No. Its documentation describes broad CSS 2.1 support with exceptions and additional limits. Verify the exact properties in the API reference before committing to a browser-only layout.

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.