[{"data":1,"prerenderedAt":921},["Reactive",2],{"/guides/html-to-pdf-python":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"dateModified":10,"author":11,"body":12,"_type":916,"_id":917,"_source":918,"_file":919,"_extension":920},"/guides/html-to-pdf-python","guides",false,"","HTML to PDF in Python: does your HTML need a browser?","HTML to PDF in Python: we ran one invoice through WeasyPrint, xhtml2pdf, Playwright and PDFShift. See which HTML needs a browser and what that costs.","2026-11-12","Cyril Nicodème",{"type":13,"children":14,"toc":907},"root",[15,32,39,76,314,325,331,336,348,368,373,384,389,405,411,424,433,438,474,486,492,523,528,534,539,683,712,717,726,762,767,780,789,802,808,821,830,835,848,877],{"type":16,"tag":17,"props":18,"children":19},"element","p",{},[20,23,30],{"type":21,"value":22},"text","In Python, picking an HTML-to-PDF library comes down to one question: does your HTML need a browser? We sent the same invoice through four Python routes. Its one inline script ran only in the two that use Chromium. So if your HTML runs JavaScript (a chart drawn by a script, totals filled in on load) or the PDF has to match what Chrome shows, you need Chromium: Playwright on your own servers, or one ",{"type":16,"tag":24,"props":25,"children":27},"code",{"className":26},[],[28],{"type":21,"value":29},"requests.post",{"type":21,"value":31}," to the PDFShift API.",{"type":16,"tag":33,"props":34,"children":36},"h2",{"id":35},"which-python-routes-got-our-test-invoice-right",[37],{"type":21,"value":38},"Which Python routes got our test invoice right?",{"type":16,"tag":17,"props":40,"children":41},{},[42,44,50,52,58,60,66,68,74],{"type":21,"value":43},"Our test file is an invoice in 7,992 bytes of HTML. A flex header sits on top. The bill-to and ship-to addresses share a two-column CSS grid. Below them, a 45-row ",{"type":16,"tag":24,"props":45,"children":47},{"className":46},[],[48],{"type":21,"value":49},"\u003Ctable>",{"type":21,"value":51}," with a ",{"type":16,"tag":24,"props":53,"children":55},{"className":54},[],[56],{"type":21,"value":57},"\u003Cthead>",{"type":21,"value":59}," runs onto page 2, and ",{"type":16,"tag":24,"props":61,"children":63},{"className":62},[],[64],{"type":21,"value":65},"tr { break-inside: avoid }",{"type":21,"value":67}," keeps each row whole. An ",{"type":16,"tag":24,"props":69,"children":71},{"className":70},[],[72],{"type":21,"value":73},"@page",{"type":21,"value":75}," rule asks for A4 and a \"Page X of Y\" footer in the bottom-right margin. One inline script writes \"JS ran\" into an empty element. Every route got that file unchanged.",{"type":16,"tag":77,"props":78,"children":80},"guide-figure",{"caption":79},"Four checks on the same 45-row invoice, one row per Python route. Our test, November 12, 2026.",[81],{"type":16,"tag":82,"props":83,"children":92},"svg",{"viewBox":84,"xmlns":85,"role":86,"ariaLabelledBy":87,"style":90,"fontFamily":91},"0 0 360 216","http://www.w3.org/2000/svg","img",[88,89],"h2p-checks-title","h2p-checks-desc","max-width:360px;display:block;margin:0 auto","DM Sans, sans-serif",[93,99,105,115,139,191,221,242,278],{"type":16,"tag":94,"props":95,"children":96},"title",{"id":88},[97],{"type":21,"value":98},"Only PDFShift and Playwright passed all four checks",{"type":16,"tag":100,"props":101,"children":102},"desc",{"id":89},[103],{"type":21,"value":104},"Check and cross matrix. PDFShift API with use_print: script ran, grid kept, Page X of Y footer, header row on page 2, all yes. Playwright: all yes. WeasyPrint: script did not run; grid, footer and header row yes. xhtml2pdf: script did not run, grid stacked, no footer; header row repeated on page 2.",{"type":16,"tag":106,"props":107,"children":114},"rect",{"x":108,"y":109,"width":110,"height":111,"rx":112,"fill":113},"4","50","352","36","6","#FFFFFF",[],{"type":16,"tag":116,"props":117,"children":119},"g",{"stroke":118},"#DBE4F2",[120,127,131,135],{"type":16,"tag":121,"props":122,"children":126},"line",{"x1":123,"y1":124,"x2":125,"y2":124},"10","46","350",[],{"type":16,"tag":121,"props":128,"children":130},{"x1":123,"y1":129,"x2":125,"y2":129},"88",[],{"type":16,"tag":121,"props":132,"children":134},{"x1":123,"y1":133,"x2":125,"y2":133},"128",[],{"type":16,"tag":121,"props":136,"children":138},{"x1":123,"y1":137,"x2":125,"y2":137},"168",[],{"type":16,"tag":116,"props":140,"children":144},{"style":141,"fill":142,"textAnchor":143},"font-size:13px","#4C556D","middle",[145,152,158,164,169,175,180,186],{"type":16,"tag":21,"props":146,"children":149},{"x":147,"y":148},"132","18",[150],{"type":21,"value":151},"Script",{"type":16,"tag":21,"props":153,"children":155},{"x":147,"y":154},"34",[156],{"type":21,"value":157},"ran",{"type":16,"tag":21,"props":159,"children":161},{"x":160,"y":148},"190",[162],{"type":21,"value":163},"Grid",{"type":16,"tag":21,"props":165,"children":166},{"x":160,"y":154},[167],{"type":21,"value":168},"kept",{"type":16,"tag":21,"props":170,"children":172},{"x":171,"y":148},"248",[173],{"type":21,"value":174},"Page X",{"type":16,"tag":21,"props":176,"children":177},{"x":171,"y":154},[178],{"type":21,"value":179},"of Y",{"type":16,"tag":21,"props":181,"children":183},{"x":182,"y":148},"312",[184],{"type":21,"value":185},"Header row",{"type":16,"tag":21,"props":187,"children":188},{"x":182,"y":154},[189],{"type":21,"value":190},"on page 2",{"type":16,"tag":116,"props":192,"children":194},{"style":141,"fill":193},"#0A0C3D",[195,203,209,215],{"type":16,"tag":21,"props":196,"children":200},{"x":197,"y":198,"style":199},"14","73","font-weight:600",[201],{"type":21,"value":202},"PDFShift API",{"type":16,"tag":21,"props":204,"children":206},{"x":197,"y":205},"113",[207],{"type":21,"value":208},"Playwright",{"type":16,"tag":21,"props":210,"children":212},{"x":197,"y":211},"153",[213],{"type":21,"value":214},"WeasyPrint",{"type":16,"tag":21,"props":216,"children":218},{"x":197,"y":217},"193",[219],{"type":21,"value":220},"xhtml2pdf",{"type":16,"tag":116,"props":222,"children":224},{"style":223},"fill:none;stroke:#6C47FF;stroke-width:3;stroke-linecap:round;stroke-linejoin:round",[225,230,234,238],{"type":16,"tag":226,"props":227,"children":229},"polyline",{"points":228},"125,68 130,73 139,63",[],{"type":16,"tag":226,"props":231,"children":233},{"points":232},"183,68 188,73 197,63",[],{"type":16,"tag":226,"props":235,"children":237},{"points":236},"241,68 246,73 255,63",[],{"type":16,"tag":226,"props":239,"children":241},{"points":240},"305,68 310,73 319,63",[],{"type":16,"tag":116,"props":243,"children":245},{"style":244},"fill:none;stroke:#0A0C3D;stroke-width:3;stroke-linecap:round;stroke-linejoin:round",[246,250,254,258,262,266,270,274],{"type":16,"tag":226,"props":247,"children":249},{"points":248},"125,108 130,113 139,103",[],{"type":16,"tag":226,"props":251,"children":253},{"points":252},"183,108 188,113 197,103",[],{"type":16,"tag":226,"props":255,"children":257},{"points":256},"241,108 246,113 255,103",[],{"type":16,"tag":226,"props":259,"children":261},{"points":260},"305,108 310,113 319,103",[],{"type":16,"tag":226,"props":263,"children":265},{"points":264},"183,148 188,153 197,143",[],{"type":16,"tag":226,"props":267,"children":269},{"points":268},"241,148 246,153 255,143",[],{"type":16,"tag":226,"props":271,"children":273},{"points":272},"305,148 310,153 319,143",[],{"type":16,"tag":226,"props":275,"children":277},{"points":276},"305,188 310,193 319,183",[],{"type":16,"tag":116,"props":279,"children":281},{"style":280},"stroke:#FF9D28;stroke-width:3;stroke-linecap:round",[282,288,291,295,298,303,306,311],{"type":16,"tag":121,"props":283,"children":287},{"x1":284,"y1":285,"x2":286,"y2":211},"127","143","137",[],{"type":16,"tag":121,"props":289,"children":290},{"x1":286,"y1":285,"x2":284,"y2":211},[],{"type":16,"tag":121,"props":292,"children":294},{"x1":284,"y1":293,"x2":286,"y2":217},"183",[],{"type":16,"tag":121,"props":296,"children":297},{"x1":286,"y1":293,"x2":284,"y2":217},[],{"type":16,"tag":121,"props":299,"children":302},{"x1":300,"y1":293,"x2":301,"y2":217},"185","195",[],{"type":16,"tag":121,"props":304,"children":305},{"x1":301,"y1":293,"x2":300,"y2":217},[],{"type":16,"tag":121,"props":307,"children":310},{"x1":308,"y1":293,"x2":309,"y2":217},"243","253",[],{"type":16,"tag":121,"props":312,"children":313},{"x1":309,"y1":293,"x2":308,"y2":217},[],{"type":16,"tag":17,"props":315,"children":316},{},[317,323],{"type":16,"tag":318,"props":319,"children":320},"strong",{},[321],{"type":21,"value":322},"Method.",{"type":21,"value":324}," One Apple Silicon Mac (macOS 27.0.1, Python 3.15), November 12, 2026. WeasyPrint 70.0, xhtml2pdf 0.2.28, Playwright 1.65.1 with its Chromium build 1254, and requests 2.34.2 for the API call. Each route printed A4 with its defaults, plus only the options in the code below. Times are the median of 3 runs.",{"type":16,"tag":33,"props":326,"children":328},{"id":327},"what-does-weasyprint-need-on-your-server",[329],{"type":21,"value":330},"What does WeasyPrint need on your server?",{"type":16,"tag":17,"props":332,"children":333},{},[334],{"type":21,"value":335},"A pip install, and a system library that pip can't install for you. The Python side is short:",{"type":16,"tag":337,"props":338,"children":343},"pre",{"className":339,"code":341,"language":342,"meta":7},[340],"language-python","from weasyprint import HTML\n\n# html can be your Django or Jinja template rendered to a string\nHTML(string=html).write_pdf(\"invoice.pdf\")\n","python",[344],{"type":16,"tag":24,"props":345,"children":346},{"__ignoreMap":7},[347],{"type":21,"value":341},{"type":16,"tag":17,"props":349,"children":350},{},[351,353,358,360,366],{"type":21,"value":352},"On our invoice, WeasyPrint got everything but the script right: the flex header, the two-column grid, the header row repeated on page 2 and the ",{"type":16,"tag":24,"props":354,"children":356},{"className":355},[],[357],{"type":21,"value":73},{"type":21,"value":359}," footer, with the table split 32 rows and then 13. It took a median 110 ms per PDF. If your HTML loads images or stylesheets by relative path, pass ",{"type":16,"tag":24,"props":361,"children":363},{"className":362},[],[364],{"type":21,"value":365},"base_url",{"type":21,"value":367}," as well.",{"type":16,"tag":17,"props":369,"children":370},{},[371],{"type":21,"value":372},"The system library is Pango. WeasyPrint 70.0 needs Pango 1.44 or newer on every server that renders PDFs. On Debian:",{"type":16,"tag":337,"props":374,"children":379},{"className":375,"code":377,"language":378,"meta":7},[376],"language-bash","apt install python3-pip libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0\n","bash",[380],{"type":16,"tag":24,"props":381,"children":382},{"__ignoreMap":7},[383],{"type":21,"value":377},{"type":16,"tag":17,"props":385,"children":386},{},[387],{"type":21,"value":388},"Installing it isn't always smooth: in an r/Python thread from January asking for HTML-to-PDF libraries, the person who started it reported a WeasyPrint \"dll issue\".",{"type":16,"tag":17,"props":390,"children":391},{},[392,394,403],{"type":21,"value":393},"Our invoice is a simple layout. ",{"type":16,"tag":395,"props":396,"children":400},"a",{"href":397,"rel":398},"https://doc.courtbouillon.org/weasyprint/stable/api_reference.html",[399],"nofollow",[401],{"type":21,"value":402},"WeasyPrint's API reference",{"type":21,"value":404}," says flex \"works for simple use cases but is not deeply tested\", and grid \"works for simple cases, but has some limitations\". Run your own template through it once before you build on it.",{"type":16,"tag":33,"props":406,"children":408},{"id":407},"why-did-xhtml2pdf-turn-the-invoice-into-3-pages",[409],{"type":21,"value":410},"Why did xhtml2pdf turn the invoice into 3 pages?",{"type":16,"tag":17,"props":412,"children":413},{},[414,416,422],{"type":21,"value":415},"xhtml2pdf is pure Python, built on ReportLab. ",{"type":16,"tag":24,"props":417,"children":419},{"className":418},[],[420],{"type":21,"value":421},"pip install xhtml2pdf",{"type":21,"value":423}," is all it takes, with nothing outside pip. It made our PDF in 75 ms.",{"type":16,"tag":337,"props":425,"children":428},{"className":426,"code":427,"language":342,"meta":7},[340],"from xhtml2pdf import pisa\n\nwith open(\"invoice.pdf\", \"wb\") as f:\n    pisa.CreatePDF(html, dest=f)\n",[429],{"type":16,"tag":24,"props":430,"children":431},{"__ignoreMap":7},[432],{"type":21,"value":427},{"type":16,"tag":17,"props":434,"children":435},{},[436],{"type":21,"value":437},"Its own log names the first problem: \"Ignoring CSS properties xhtml2pdf does not implement: border-collapse, grid-template-columns\". Its docs say it treats grid as a plain block, so the bill-to and ship-to addresses stacked on top of each other. The fonts came out different, the item descriptions wrapped onto two lines, and the 45 rows needed 3 pages (13, 18 and 14 rows). The header row did repeat on pages 2 and 3.",{"type":16,"tag":17,"props":439,"children":440},{},[441,443,448,450,456,458,464,466,472],{"type":21,"value":442},"The ",{"type":16,"tag":24,"props":444,"children":446},{"className":445},[],[447],{"type":21,"value":73},{"type":21,"value":449}," footer never printed. xhtml2pdf takes page numbers from its own tags, ",{"type":16,"tag":24,"props":451,"children":453},{"className":452},[],[454],{"type":21,"value":455},"\u003Cpdf:pagenumber />",{"type":21,"value":457}," and ",{"type":16,"tag":24,"props":459,"children":461},{"className":460},[],[462],{"type":21,"value":463},"\u003Cpdf:pagecount />",{"type":21,"value":465},", placed in an ",{"type":16,"tag":24,"props":467,"children":469},{"className":468},[],[470],{"type":21,"value":471},"@frame footer",{"type":21,"value":473},".",{"type":16,"tag":17,"props":475,"children":476},{},[477,484],{"type":16,"tag":395,"props":478,"children":481},{"href":479,"rel":480},"https://xhtml2pdf.readthedocs.io/en/latest/",[399],[482],{"type":21,"value":483},"Its docs",{"type":21,"value":485}," promise HTML5 and CSS 2.1 \"and some of CSS 3\". So for HTML written for the web, the pip-only install doesn't save you work: you'd keep a second copy of the template written for xhtml2pdf, and update both every time.",{"type":16,"tag":33,"props":487,"children":489},{"id":488},"is-pdfkit-still-worth-using",[490],{"type":21,"value":491},"Is pdfkit still worth using?",{"type":16,"tag":17,"props":493,"children":494},{},[495,497,503,505,512,514,521],{"type":21,"value":496},"No. The top answer on a Stack Overflow question about this still uses ",{"type":16,"tag":24,"props":498,"children":500},{"className":499},[],[501],{"type":21,"value":502},"pdfkit.from_url",{"type":21,"value":504},", but pdfkit is a wrapper around wkhtmltopdf, and wkhtmltopdf's ",{"type":16,"tag":395,"props":506,"children":509},{"href":507,"rel":508},"https://github.com/wkhtmltopdf/wkhtmltopdf",[399],[510],{"type":21,"value":511},"GitHub repository was archived on January 2, 2023",{"type":21,"value":513},". Its ",{"type":16,"tag":395,"props":515,"children":518},{"href":516,"rel":517},"https://wkhtmltopdf.org/status.html",[399],[519],{"type":21,"value":520},"status page",{"type":21,"value":522}," says the WebKit inside it \"hasn't been updated since 2012\", and warns that running it on untrusted HTML \"can lead to complete takeover of the server it is running on\". pdfkit's last release, 1.0.0, came out in November 2021. We left it out of our test.",{"type":16,"tag":17,"props":524,"children":525},{},[526],{"type":21,"value":527},"pyhtml2pdf is the other wrapper that comes up. It drives headless Chrome through Selenium, so it's the Chromium route below with Selenium added on top.",{"type":16,"tag":33,"props":529,"children":531},{"id":530},"what-does-running-chromium-from-python-cost",[532],{"type":21,"value":533},"What does running Chromium from Python cost?",{"type":16,"tag":17,"props":535,"children":536},{},[537],{"type":21,"value":538},"About 632 MB on our Mac before the first PDF, and a browser process you keep running after it.",{"type":16,"tag":77,"props":540,"children":542},{"caption":541},"Install size on disk for each Python route, wheels plus files outside pip. Our test on an Apple Silicon Mac, November 12, 2026.",[543],{"type":16,"tag":82,"props":544,"children":549},{"viewBox":545,"xmlns":85,"role":86,"ariaLabelledBy":546,"style":90,"fontFamily":91},"0 0 360 280",[547,548],"h2p-size-title","h2p-size-desc",[550,555,560,579,626,634,638,643,649,654],{"type":16,"tag":94,"props":551,"children":552},{"id":547},[553],{"type":21,"value":554},"Playwright takes about 632 MB on disk; the PDFShift route takes 0.77 MB",{"type":16,"tag":100,"props":556,"children":557},{"id":548},[558],{"type":21,"value":559},"Horizontal bar chart of install size in MB, starting at zero. PDFShift API through requests: 0.77 MB, 5 wheels. WeasyPrint: 11.7 MB of wheels plus the Pango system library, which was not measured. xhtml2pdf: 16.5 MB. Playwright: 48 MB of wheels plus about 584 MB of browser files, about 632 MB in all.",{"type":16,"tag":116,"props":561,"children":562},{"stroke":118},[563,567,571,576],{"type":16,"tag":121,"props":564,"children":566},{"x1":123,"y1":108,"x2":123,"y2":565},"252",[],{"type":16,"tag":121,"props":568,"children":570},{"x1":123,"y1":565,"x2":569,"y2":565},"330",[],{"type":16,"tag":121,"props":572,"children":575},{"x1":573,"y1":565,"x2":573,"y2":574},"170","257",[],{"type":16,"tag":121,"props":577,"children":578},{"x1":569,"y1":565,"x2":569,"y2":574},[],{"type":16,"tag":116,"props":580,"children":581},{"style":141,"fill":142},[582,588,593,598,603,609,614,620],{"type":16,"tag":21,"props":583,"children":585},{"x":123,"y":584,"textAnchor":143},"272",[586],{"type":21,"value":587},"0",{"type":16,"tag":21,"props":589,"children":590},{"x":573,"y":584,"textAnchor":143},[591],{"type":21,"value":592},"320",{"type":16,"tag":21,"props":594,"children":595},{"x":569,"y":584,"textAnchor":143},[596],{"type":21,"value":597},"640 MB",{"type":16,"tag":21,"props":599,"children":600},{"x":148,"y":148,"fill":193,"style":199},[601],{"type":21,"value":602},"PDFShift API (requests)",{"type":16,"tag":21,"props":604,"children":606},{"x":148,"y":605},"76",[607],{"type":21,"value":608},"WeasyPrint (+ Pango, a system library)",{"type":16,"tag":21,"props":610,"children":612},{"x":148,"y":611},"134",[613],{"type":21,"value":220},{"type":16,"tag":21,"props":615,"children":617},{"x":148,"y":616},"192",[618],{"type":21,"value":619},"Playwright (wheels + browser files)",{"type":16,"tag":21,"props":621,"children":623},{"x":148,"y":622},"240",[624],{"type":21,"value":625},"48 MB of wheels + 584 MB = about 632 MB",{"type":16,"tag":106,"props":627,"children":633},{"x":123,"y":628,"width":629,"height":630,"rx":631,"fill":632},"26","2","22","1","#6C47FF",[],{"type":16,"tag":106,"props":635,"children":637},{"x":123,"y":636,"width":112,"height":630,"rx":629,"fill":142},"84",[],{"type":16,"tag":106,"props":639,"children":642},{"x":123,"y":640,"width":641,"height":630,"rx":629,"fill":142},"142","8",[],{"type":16,"tag":106,"props":644,"children":648},{"x":123,"y":645,"width":646,"height":630,"rx":647,"fill":142},"200","24","3",[],{"type":16,"tag":106,"props":650,"children":653},{"x":154,"y":645,"width":651,"height":630,"rx":647,"fill":652},"292","#FF9D28",[],{"type":16,"tag":116,"props":655,"children":656},{"style":141,"fill":193},[657,664,670,676],{"type":16,"tag":21,"props":658,"children":661},{"x":659,"y":660,"style":199},"20","42",[662],{"type":21,"value":663},"0.77 MB (5 wheels)",{"type":16,"tag":21,"props":665,"children":667},{"x":646,"y":666},"100",[668],{"type":21,"value":669},"11.7 MB + Pango (not measured)",{"type":16,"tag":21,"props":671,"children":673},{"x":628,"y":672},"158",[674],{"type":21,"value":675},"16.5 MB",{"type":16,"tag":21,"props":677,"children":680},{"x":678,"y":679},"44","216",[681],{"type":21,"value":682},"about 584 MB of browser files",{"type":16,"tag":17,"props":684,"children":685},{},[686,688,694,696,702,704,710],{"type":21,"value":687},"Playwright's 48 MB of wheels (the pip packages) are the small part. The default ",{"type":16,"tag":24,"props":689,"children":691},{"className":690},[],[692],{"type":21,"value":693},"playwright install chromium",{"type":21,"value":695}," then downloaded about 584 MB more: the full Chromium (376 MB), the headless shell (205 MB) and ffmpeg (2.9 MB). ",{"type":16,"tag":24,"props":697,"children":699},{"className":698},[],[700],{"type":21,"value":701},"page.pdf()",{"type":21,"value":703}," only needs the headless shell, and ",{"type":16,"tag":24,"props":705,"children":707},{"className":706},[],[708],{"type":21,"value":709},"--only-shell",{"type":21,"value":711}," skips the full Chromium. That still leaves 205 MB of browser to deploy, keep updated and run.",{"type":16,"tag":17,"props":713,"children":714},{},[715],{"type":21,"value":716},"This is the Playwright code we ran, with the sync API:",{"type":16,"tag":337,"props":718,"children":721},{"className":719,"code":720,"language":342,"meta":7},[340],"from playwright.sync_api import sync_playwright\n\nwith sync_playwright() as p:\n    browser = p.chromium.launch()  # launch once, reuse for every PDF\n    page = browser.new_page()\n    page.set_content(html, wait_until=\"load\")\n    page.pdf(\n        path=\"invoice.pdf\",\n        format=\"A4\",\n        print_background=True,\n        prefer_css_page_size=True,\n    )\n    page.close()\n    browser.close()\n",[722],{"type":16,"tag":24,"props":723,"children":724},{"__ignoreMap":7},[725],{"type":21,"value":720},{"type":16,"tag":17,"props":727,"children":728},{},[729,731,737,739,745,747,753,755,760],{"type":21,"value":730},"Each option changes a default. Without ",{"type":16,"tag":24,"props":732,"children":734},{"className":733},[],[735],{"type":21,"value":736},"format",{"type":21,"value":738},", Playwright prints US Letter. Without ",{"type":16,"tag":24,"props":740,"children":742},{"className":741},[],[743],{"type":21,"value":744},"print_background=True",{"type":21,"value":746},", background colors don't print. Without ",{"type":16,"tag":24,"props":748,"children":750},{"className":749},[],[751],{"type":21,"value":752},"prefer_css_page_size=True",{"type":21,"value":754},", it ignores the page size in your ",{"type":16,"tag":24,"props":756,"children":758},{"className":757},[],[759],{"type":21,"value":73},{"type":21,"value":761}," rule.",{"type":16,"tag":17,"props":763,"children":764},{},[765],{"type":21,"value":766},"Playwright passed all four checks on 2 pages, 28 rows on the first and 17 on the second. It took 76 ms per PDF with the browser already open. Launching the browser took 1.4 s, so start it once when your worker starts and open a new page for each PDF.",{"type":16,"tag":17,"props":768,"children":769},{},[770,772,778],{"type":21,"value":771},"In a Docker image, ",{"type":16,"tag":24,"props":773,"children":775},{"className":774},[],[776],{"type":21,"value":777},"--with-deps",{"type":21,"value":779}," adds the system libraries the browser needs:",{"type":16,"tag":337,"props":781,"children":784},{"className":782,"code":783,"language":378,"meta":7},[376],"RUN playwright install --with-deps --only-shell\n",[785],{"type":16,"tag":24,"props":786,"children":787},{"__ignoreMap":7},[788],{"type":21,"value":783},{"type":16,"tag":17,"props":790,"children":791},{},[792,794,800],{"type":21,"value":793},"From then on, a browser is part of your production stack, and you handle its memory use and timeouts. Our ",{"type":16,"tag":395,"props":795,"children":797},{"href":796},"/guides/playwright-pdf-in-production/",[798],{"type":21,"value":799},"Playwright production guide",{"type":21,"value":801}," covers both.",{"type":16,"tag":33,"props":803,"children":805},{"id":804},"how-do-you-get-chromiums-pdf-in-python-without-running-a-browser",[806],{"type":21,"value":807},"How do you get Chromium's PDF in Python without running a browser?",{"type":16,"tag":17,"props":809,"children":810},{},[811,813,819],{"type":21,"value":812},"Send the HTML to the PDFShift API. We run Chromium, and the PDF comes back as the response body. On your side, that's ",{"type":16,"tag":24,"props":814,"children":816},{"className":815},[],[817],{"type":21,"value":818},"requests",{"type":21,"value":820}," and nothing else:",{"type":16,"tag":337,"props":822,"children":825},{"className":823,"code":824,"language":342,"meta":7},[340],"import os\nimport requests\n\n# html can be your Django or Jinja template rendered to a string\nresponse = requests.post(\n    \"https://api.pdfshift.io/v3/convert/pdf\",\n    headers={\"X-API-Key\": os.environ[\"PDFSHIFT_API_KEY\"]},\n    json={\"source\": html, \"format\": \"A4\", \"use_print\": True},\n)\nresponse.raise_for_status()\n\nwith open(\"invoice.pdf\", \"wb\") as f:\n    f.write(response.content)\n",[826],{"type":16,"tag":24,"props":827,"children":828},{"__ignoreMap":7},[829],{"type":21,"value":824},{"type":16,"tag":17,"props":831,"children":832},{},[833],{"type":21,"value":834},"Like Playwright, it passed all four checks on 2 pages, in 1.8 s with the network round trip.",{"type":16,"tag":17,"props":836,"children":837},{},[838,840,846],{"type":21,"value":839},"Keep ",{"type":16,"tag":24,"props":841,"children":843},{"className":842},[],[844],{"type":21,"value":845},"use_print: True",{"type":21,"value":847}," in the body. Without it, our PDF lost the header row on page 2.",{"type":16,"tag":17,"props":849,"children":850},{},[851,853,859,861,867,869,875],{"type":21,"value":852},"You can run that code before you sign up. Leave out the ",{"type":16,"tag":24,"props":854,"children":856},{"className":855},[],[857],{"type":21,"value":858},"X-API-Key",{"type":21,"value":860}," header and the call runs as a free account: the PDF gets a watermark, and the limits are 2 MB, 30 seconds and 10 calls a day. That's how we ran our test. While you build, set ",{"type":16,"tag":24,"props":862,"children":864},{"className":863},[],[865],{"type":21,"value":866},"sandbox",{"type":21,"value":868}," to ",{"type":16,"tag":24,"props":870,"children":872},{"className":871},[],[873],{"type":21,"value":874},"True",{"type":21,"value":876}," to get watermarked PDFs that don't use your credits.",{"type":16,"tag":17,"props":878,"children":879},{},[880,882,888,890,897,899,905],{"type":21,"value":881},"With a key, the free plan gives you 50 credits a month, no credit card. ",{"type":16,"tag":395,"props":883,"children":885},{"href":884},"/register/",[886],{"type":21,"value":887},"Get a free API key",{"type":21,"value":889},", and find every parameter in the ",{"type":16,"tag":395,"props":891,"children":894},{"href":892,"rel":893},"https://docs.pdfshift.io/",[399],[895],{"type":21,"value":896},"API docs",{"type":21,"value":898},". If your project uses aiohttp, urllib3 or httplib2 instead of requests, ",{"type":16,"tag":395,"props":900,"children":902},{"href":901},"/guides/python/",[903],{"type":21,"value":904},"our Python guides",{"type":21,"value":906}," show the same call with each client.",{"title":7,"searchDepth":908,"depth":908,"links":909},2,[910,911,912,913,914,915],{"id":35,"depth":908,"text":38},{"id":327,"depth":908,"text":330},{"id":407,"depth":908,"text":410},{"id":488,"depth":908,"text":491},{"id":530,"depth":908,"text":533},{"id":804,"depth":908,"text":807},"markdown","content:guides:html-to-pdf-python.md","content","guides/html-to-pdf-python.md","md",1794490384352]