DEV Community

IronSoftware
IronSoftware

Posted on

fpdf2 in Python: What the Code-First Path Costs Later

$ pip install fpdf2
Enter fullscreen mode Exit fullscreen mode
from fpdf import FPDF

pdf = FPDF()
pdf.add_page()
pdf.set_font("Helvetica", size=14)
pdf.cell(0, 10, "Invoice #1042", ln=True)
pdf.output("invoice.pdf")
Enter fullscreen mode Exit fullscreen mode

One command pulling a 329 KB pure-Python wheel and three dependencies behind it, then seven lines putting a one-page PDF on disk at 14 point, with no compiled extension anywhere in the install and no browser process at run time. Read the two blocks together and they are the whole argument for fpdf2, which is also where the argument against it lives, because every element on that page costs another method call, written in the order the page is drawn. An invoice that already exists as HTML gets rebuilt field by field before fpdf2 can produce it, rather than going to a renderer as it stands.

Full disclosure. IronPDF is what our team at Iron Software builds. This piece examines what fpdf2's absent CSS path and Python 3.10 floor cost later, and how IronPDF turns an existing template into a PDF without a rebuild.

What Does a Code-First Path Cost Later?

The first boundary is markup. The write_html() method covers a fixed tag set, covering headings, paragraphs, basic tables, lists, links, and a handful of inline styles, with no general CSS support beyond page-break hints. The fpdf2 documentation states that the whole HTML 5 specification is not supported and neither is CSS, and it points readers toward other libraries for that job. A design that already exists as a web page, an email template, or a stylesheet-driven layout gets rebuilt by hand through the cell and layout API rather than handed to a renderer as it stands.

The second is round-tripping. fpdf2 generates documents rather than reading them, and the project ships a dedicated Combine with pypdf page for exactly that reason. Any workflow that opens, stamps, or merges a document that already exists therefore carries two dependencies with two release cadences and two security surfaces, which is a maintenance cost that shows up years after the choice was made.

How Little Does the Install Actually Need?

Nothing about fpdf2 is stronger than its install, and that is not a small point. The current release is a 329 KB pure-Python wheel with three required dependencies, namely defusedxml, Pillow, and fonttools, and no compiled extensions, so it installs the same way on a locked-down Windows box, an Alpine container, and a Lambda function. Underneath sit more than 1,300 unit tests running on Linux and Windows with qpdf-based PDF diffing plus timing and memory checks, validation against PDF Checker and VeraPDF, and real depth for the size, covering Unicode font embedding, SVG import, barcodes, tables, encryption, digital signing, and PDF/A output. For a document that never leaves the code that generates it, that footprint is the one case for keeping a constructor rather than a renderer in the dependency list, though few pipelines stay that self-contained for long, and IronPDF answers the markup half of the same question from one package.

None of that is bought with dependency weight, which is the trade the whole library is built around, and the same page setup arrives as render options on a markup-first path.

Capability fpdf2 2.8.8 IronPDF for Python
Existing HTML and CSS as input write_html() tag subset, no CSS beyond break hints Chromium through ChromePdfRenderer, CSS and JavaScript included
Documents built from code Procedural placement, cell and output Data merged into a template, then RenderHtmlAsPdf
Opening a PDF that already exists Cannot parse existing files, pypdf recommended alongside Load, stamp, and merge on the same PdfDocument
Install footprint 329 KB pure-Python wheel, 3 required dependencies 1 package, 0 system dependencies, engine included
Python versions accepted 3.10 and later 3.7 and later
Licence LGPL-3.0-only Commercial, one tier
Release record 2.8.8 on 9 August 2026, 4 releases in 12 months 2026.9.0.2 on 1 September 2026, 11 in 12 months
Document features Tables, barcodes, SVG import, encryption, signing, PDF/A-1, PDF/A-2, PDF/A-3 Browser-accurate layout, page control, forms, and signing

Table 1. Document input and output paths only, fpdf2 against IronPDF for Python, as each project documents itself.

Migrations are decided by the markup row and the round-tripping row, since stamping a document that already exists is where the second dependency joins. The others describe two defensible starting points rather than a gap.

Two Constraints That Come From Outside Your Code

The interpreter floor is the first. fpdf2 requires Python 3.10 or later, so a codebase still running 3.8 or 3.9, which is ordinary in estates that upgrade on a slower cycle, cannot install the current release until the runtime itself moves. That turns a library upgrade into a platform upgrade, and it lands on exactly the older services least likely to be scheduled for one.

The licence is the second. fpdf2 ships under the LGPL, stated in the README badge and reported as LGPL-3.0 by GitHub's licence detector. That is a different family from the MIT and BSD terms common across the other Python PDF libraries, and the licence text defines an Application as any work that makes use of an interface provided by the Library but which is not otherwise based on the Library, which is the language that governs code merely importing the package. What that means for a specific codebase is a question for counsel rather than a comparison article, and the text is public for anyone who needs the close read. IronPDF is licensed commercially, so the terms sit in the purchase rather than in the obligations attached to the code that imports it.

The security record is clean. The GitHub Advisory Database, Snyk, the NVD, and OSV.dev return nothing against fpdf2's own code, and the long-dormant PyFPDF it forked from is equally clear. CVEs do surface against the similarly named PHP libraries FPDF and FPDI, which share ancestry but not a codebase, so a scanner matching on the name alone produces noise somebody has to answer for, which is worth settling in the same review as the metadata and permissions on the output. The project sits in the py-pdf GitHub organization alongside pypdf, and its CI workflow runs zizmor, pylint, bandit, semgrep, grype, and guarddog on every change, which is more security tooling than most volunteer-maintained libraries carry.

What Happens When the Source Is Already a Page?

What moves a team is the first boundary made concrete. A report template that already exists as HTML with a stylesheet, an invoice designed in a browser rather than assembled field by field, or a page that needs its script to run before the layout is complete.

from ironpdf import *

renderer = ChromePdfRenderer()
pdf = renderer.RenderHtmlFileAsPdf("invoice_template.html")
pdf.SaveAs("invoice.pdf")
Enter fullscreen mode Exit fullscreen mode

That renders the template file exactly as a browser would paint it, stylesheet included, and writes it to disk. The same ChromePdfRenderer takes a string in memory through RenderHtmlAsPdf or a live page through its URL, so the entry point does not change when the markup moves.

IronPDF turns the template a designer already owns into the finished PDF without rebuilding it field by field, runs on Python 3.7 and later so the interpreter floor never gates the upgrade, and opens the documents that already exist rather than handing that job to a second library. The narrow case that stays with fpdf2 is a document assembled entirely in code on a runtime already at 3.10. IronPDF has a free trial if you want to put one of your existing templates through it before rebuilding it by hand.

So which does your document actually start as, code or a page? Tell us in the comments which one your last project needed, especially if you ended up carrying fpdf2 and a second library side by side to cover both.

fpdf2 and PyFPDF are the property of their maintainers, and we have no affiliation with the py-pdf organization. The release, licence, and security details above rest on the project's own documentation, repository, and PyPI metadata at the time of writing. If a detail has changed since, please let us know below.

Top comments (0)