Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Use WeasyPrint’s CSS(url=...) constructor and pass the resulting stylesheet to HTML.write_pdf(). Give string-based HTML a base_url whenever it contains relative images, fonts, or other resources. The default fetcher can retrieve HTTP and file URLs, but it does not handle advanced cookies or authentication; protected resources require a custom URL fetcher.
Load a remote stylesheet in one call
Install WeasyPrint using the method appropriate for your operating system, then create an HTML object and a CSS object. This complete example renders an HTML string and downloads its stylesheet over HTTPS:
from weasyprint import HTML, CSS
html = HTML(
string="""
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Invoice</title>
</head>
<body>
<h1>Invoice</h1>
<p>Generated as a PDF.</p>
</body>
</html>
""",
base_url="https://example.com/",
)
css = CSS(url="https://example.com/static/pdf.css")
html.write_pdf("output.pdf", stylesheets=[css])
CSS(url=...) tells WeasyPrint where to fetch the stylesheet. The stylesheets list accepts one or more stylesheet objects; later stylesheets can override earlier declarations according to normal CSS cascade rules. The URL should be absolute when the file is hosted remotely.
Choose the input form that matches your document
Remote HTML page with a linked stylesheet
If the entire document is public and already contains a normal link such as <link rel="stylesheet" href="/static/pdf.css">, let WeasyPrint fetch the page:
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchPC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11#1 Best Overall
from weasyprint import HTML
HTML(url="https://example.com/invoice").write_pdf("invoice.pdf")
The page’s own URL supplies the origin used to resolve relative links. You can also add an extra stylesheet:
from weasyprint import HTML, CSS
HTML(url="https://example.com/invoice").write_pdf(
"invoice.pdf",
stylesheets=[CSS(url="https://example.com/static/print-overrides.css")],
)
HTML string plus remote CSS
Use this form when your application builds the markup itself. A string has no natural document location, so set base_url for relative URLs in the HTML. Without it, references such as images/logo.svg or fonts/body.woff2 may be invalid.
from weasyprint import HTML, CSS
html = HTML(
string=rendered_html,
base_url="https://example.com/",
)
stylesheet = CSS(url="https://cdn.example.com/pdf/print.css")
html.write_pdf("report.pdf", stylesheets=[stylesheet])
The base URL applies to resources referenced by the HTML. A stylesheet’s own relative url(...) references are resolved relative to the stylesheet URL, which is another reason to use an absolute CSS URL.
Local HTML file and remote CSS
from weasyprint import HTML, CSS
HTML(filename="invoice.html").write_pdf(
"invoice.pdf",
stylesheets=[CSS(url="https://example.com/static/pdf.css")],
)
A local file gives the HTML a file-based origin. Keep the remote stylesheet absolute and verify that the rendering machine is allowed to make outbound HTTPS requests.
Make relative images, fonts, and backgrounds resolve
There are two independent URL contexts:
- HTML context:
base_urlresolves relativesrc,href, and inline CSS URLs originating in the HTML string. - Stylesheet context: the URL passed to
CSSgives external stylesheets a location for relative assets such asurl("fonts/inter.woff2")and background images.
Prefer a directory-like base URL that reflects the paths used by the markup:
Rank #2
html = HTML(
string='<img src="assets/logo.png">',
base_url="https://example.com/invoices/",
)
css = CSS(url="https://example.com/invoices/pdf/print.css")
If you cannot guarantee a useful base URL, rewrite resource references as absolute URLs. URL resolution does not guarantee that an asset will load: DNS, TLS, redirects, HTTP status, and the asset’s own availability still matter.
When the stylesheet needs cookies or authentication
WeasyPrint’s default fetcher natively opens file and HTTP URLs, but its HTTP client does not provide advanced cookie or authentication support. A private CSS endpoint that requires a session cookie, bearer token, client certificate, or special request headers therefore needs a custom URL fetcher.
A fetcher receives a URL and returns a response dictionary containing a file-like object and metadata. Delegate ordinary URLs to the default fetcher and handle only the protected host yourself. The exact implementation should follow the WeasyPrint version installed in your environment, because fetcher signatures and supported options are version-sensitive.
Recommended Free Tools
from io import BytesIO
import requests
from weasyprint import HTML, CSS, default_url_fetcher
def authenticated_fetcher(url, timeout=30):
if url.startswith("https://private.example.com/"):
response = requests.get(
url,
headers={"Authorization": "Bearer YOUR_TOKEN"},
timeout=timeout,
)
response.raise_for_status()
return {
"string": response.content,
"mime_type": response.headers.get("Content-Type", ""),
"redirected_url": response.url,
}
return default_url_fetcher(url)
html = HTML(
string="<html><body><h1>Private report</h1></body></html>",
base_url="https://private.example.com/",
url_fetcher=authenticated_fetcher,
)
css = CSS(
url="https://private.example.com/pdf/print.css",
url_fetcher=authenticated_fetcher,
)
html.write_pdf("private-report.pdf", stylesheets=[css])
Do not hard-code production secrets. Read tokens from a secret manager or environment variable, restrict which hosts the fetcher may contact, and avoid forwarding credentials to redirects on a different origin.
Decide whether a fetch failure should warn or stop
By default, fetch errors are caught and reported as warnings; WeasyPrint may continue and produce a PDF without the missing stylesheet or asset. That is useful for best-effort documents but dangerous for invoices, legal forms, or branded reports where unstyled output is invalid.
Fail explicitly for a required CSS file
In a custom fetcher, catch the request error for the stylesheet and raise WeasyPrint’s fatal URL-fetching exception. The exception name and import path should be checked against your installed release. The important policy is to distinguish required CSS from optional images and to fail the job when required CSS cannot be downloaded.
from weasyprint import default_url_fetcher
# Import the fatal URL-fetching exception from the WeasyPrint release you use.
def strict_fetcher(url, timeout=20):
try:
return default_url_fetcher(url)
except Exception as exc:
if url.endswith("/pdf.css"):
# raise FatalURLFetchingError(url, str(exc)) from exc
raise
raise
In production, log the URL, status, redirect target, and exception while removing secrets. A successful HTTP response is not enough: verify that the response is CSS and that the resulting PDF has the expected layout.
Command-line equivalent
The WeasyPrint command-line interface accepts a stylesheet URL or filename with -s or --stylesheet:
weasyprint input.html output.pdf
--stylesheet https://example.com/static/pdf.css
Use -u or --base-url when relative references in the input need an explicit origin. The CLI also exposes options for timeouts, allowed protocols, redirect behavior, and failing on HTTP errors. These flags can vary by release, so run weasyprint --help on the installed version and consult its matching reference before putting a flag into automation.
CSS that works on screen but not in a PDF
- Use print rules: put PDF-specific declarations in the remote stylesheet, including
@page, margins, page size, and print colors. - Expect pagination: long flex or grid layouts, fixed-position elements, and very large unbreakable blocks can produce unexpected page breaks.
- Check fonts: a font URL in
@font-facemust be reachable from the renderer and permitted by the fetcher. - Keep media intent clear: the stylesheet passed to
write_pdfis a user stylesheet; use it for print overrides rather than assuming browser-only behavior.
Render a small fixture first, inspect the PDF, and then add complex assets. This separates URL-fetching problems from pagination and CSS compatibility problems.
Security and network controls
Rendering untrusted HTML or CSS in a server application is security-sensitive. User-controlled URLs can attempt to reach internal services, local files, cloud metadata endpoints, or unexpectedly large resources. Restrict allowed schemes and hosts, isolate the renderer, set timeouts, limit response sizes, and disable access that your application does not need. The CLI’s allowed-protocol controls and your custom fetcher’s host allowlist should reflect the actual threat model.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Also verify outbound DNS and TLS from the worker that creates the PDF. A URL that works in your browser may fail in a container with no network route or missing certificate authorities. Treat redirects carefully and do not allow a public stylesheet URL to redirect into a private network.
Performance, caching, and repeatability
Every uncached remote stylesheet and asset adds network latency to a render. Keep CSS and fonts close to the worker, use stable URLs, and avoid loading resources that the document does not use. If your application renders many documents, fetch approved assets ahead of time and serve them from an internal, controlled origin, or implement a bounded cache in the custom fetcher.
Pin your WeasyPrint version and test representative documents after upgrades. Record the stylesheet revision alongside generated documents when reproducibility matters. A remote URL can change without any code deployment, so immutable asset URLs or content-addressed files are safer for archival PDFs.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| PDF has default browser-like styling | CSS URL was not passed, or the fetch failed and only a warning was emitted. | Construct CSS(url=...), pass it in stylesheets, and inspect warnings or make required CSS failures fatal. |
| Images or fonts are missing | Relative references in string HTML have no base URL. | Set base_url on HTML, or convert references to absolute URLs. |
| CSS loads but its background image does not | The image path is relative to a different stylesheet location, or the asset is unavailable. | Use an absolute CSS(url=...), check the relative path, and test the asset from the worker. |
| 401 or 403 response | The default fetcher lacks the required cookie or authorization header. | Supply a custom URL fetcher with controlled credentials and timeouts. |
| Render hangs or is very slow | DNS, TLS, a redirect loop, or an unresponsive resource. | Set fetch timeouts, inspect redirects, restrict hosts, and remove unnecessary remote assets. |
| CLI rejects an option | The flag differs in the installed WeasyPrint release. | Run weasyprint --help and use the reference for that exact version. |
Or skip the browser setup
If your real goal is a clean image or PDF of a public web page rather than a Python-rendered document with your own CSS, ScreenshotNeo provides a single HTTP request. It accepts 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 or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
Free tools Windows power users keep installed
One-click scans. No signup required.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo documentation for all options. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Sign up at ScreenshotNeo.
Best Value
Python and Node.js calls for ScreenshotNeo
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}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
ScreenshotNeo is not a replacement for WeasyPrint when you need to compose arbitrary HTML and a private stylesheet inside Python. It is the simpler route for capturing an already-hosted page, with optional full-page, device, PDF, waiting, blocking, authentication, caching, and webhook controls.
Frequently Asked Questions
Can I pass a CSS URL directly to write_pdf?
Create a stylesheet with CSS(url=”https://…”) and pass that object in the stylesheets list; write_pdf does not require a raw URL string in that argument.
Why does a remote page work while HTML(string=…) does not?
A remote page has its own document URL for resolving relative resources. String HTML does not, so provide base_url or use absolute resource URLs.
Should a missing stylesheet always abort generation?
Not necessarily. Warnings are acceptable for optional styling, but documents whose layout or compliance depends on CSS should use a custom fetcher policy that raises a fatal fetching error.
Quick Recap
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.

