What's the best Python QR code generator library?
There are quite a few Python libraries that can generate QR codes, and the popularity rankings don’t line up with the technical merits. I benchmarked seven of them for correctness, speed, install footprint and symbol compactness. This post summarises the results, contextualised through my own experience in the field, and shows how to use the four you should consider. The full benchmark report has the complete numbers.
Full disclosure up front: I am the maintainer of pyStrich. Every effort has been made to make the comparisons fair and representative - the corpus and validity rules are encoder-neutral, and the raw records can be inspected alongside the report. Feedback and corrections are welcome.
TLDR
pyStrich is the best general purpose option. It’s the fastest pure Python QR code generator, it has outputs for PNG, SVG, EPS and DXF, and it has comprehensive documentation. QR codes are one of several symbologies (barcode types) it supports, alongside Data Matrix, Aztec, PDF417, Code 128, Code 39, EAN-13 and ITF. It generates QR codes that are compact and reliably readable by default without you having to worry about how encoding works because it takes care of charset selection, specifying ECI and picking the most efficient modes.
zxing-cpp (which uses zint for barcode encoding) is the fastest option in most cases, but it’s written in C/C++ with Python bindings rather than pure Python. It supports PNG (rasterised by the library, written through Pillow) and SVG, and it also decodes barcodes - useful if you need generating and scanning from one dependency. Documentation is limited, especially for encoding.
segno is another strong pure Python option, with the smallest install footprint of the practical choices (it writes its own PNG and SVG, no Pillow needed). It has helpers and recipes for payloads commonly used with QR codes (WiFi, vCard, EPC and so on) and supports niche use cases around and outside standard QR codes, such as Micro QR. Good documentation. Its main weakness in this benchmark: no mixed-mode encoding (switching encoding modes within one symbol), which produces larger symbols for some payloads.
qrcode (python-qrcode) is the most popular option by download count, but it has no ECI support - non-ASCII payloads are emitted as unattributed UTF-8 bytes and there is no kanji mode. Plenty of decoders will guess their way through that, but you’re relying on the reader’s heuristics rather than encoding the data properly. It was also the slowest of the four here at most payload sizes.
Avoid treepoem for this job. It spawns a Ghostscript process per barcode, which makes it orders of magnitude slower than the alternatives (2.3 to 5 seconds per QR code in this benchmark, against milliseconds for everything else) and adds a heavyweight non-Python dependency.
Also: qrcodegen (tiny, no file output of its own - you render the module grid yourself) and opencv-python-headless (works, but it’s not fast and it’d only be worth using if you already had OpenCV installed).
Performance benchmark results
The corpus was 26 QR code cases covering numeric, alphanumeric, Latin-1, UTF-8, kanji and mixed-mode payloads from 9 to 2,510 characters, at explicit error correction levels, each generated as PNG and as SVG. Every output was decode-verified against its payload.
Correctness. pyStrich generated all 26 cases correctly in both formats. zxing-cpp and segno each got 24 right and stumbled on the same 2 - cases built to demonstrate the ECI issue: a Latin-1 payload of currency and typographic symbols (£ ¥ ° ± ½) whose bytes also read as valid Shift-JIS katakana. Emitting those bytes with no ECI declaration is technically to spec - a QR reader is meant to default to ISO-8859-1 - but some real readers guess the charset instead, and the reference decoder here read the symbols back as katakana. pyStrich avoids the ambiguity by declaring the charset with an explicit ECI, which is the safer choice in practice. qrcode completed 22: with no ECI it can’t declare a charset, so the cases that ask for an explicit one are recorded unsupported. It still encodes those payloads, just as undeclared UTF-8 - a bulkier fallback that shows up under compactness below.
Install size. Measured as the container image increase over a shared Python base, including dependencies such as Pillow where the library renders through it:
| library | install size | cold import |
|---|---|---|
| segno | 9.2 MB | 36 ms |
| qrcode | 30.3 MB | 56 ms |
| pyStrich | 31.8 MB | 36 ms |
| zxing-cpp | 31.9 MB | 32 ms |
| treepoem | 101.5 MB | 33 ms |
segno is the standout: it writes its own PNG and SVG, so it needs no Pillow. treepoem’s figure includes Ghostscript.
Speed. zxing-cpp was fastest on almost every PNG case, with medians from 0.55 ms on a short numeric payload to about 14 ms near capacity. pyStrich was the fastest pure Python option throughout (1.1 to 61 ms), roughly two to four times faster than segno and qrcode at every payload size. treepoem took 2.3 to 5 seconds per symbol, dominated by the Ghostscript subprocess. On SVG output the picture shifts at the top end: pyStrich overtook zxing-cpp on the near-capacity payloads (around 38-54 ms against 57-59 ms).
Compactness. Measured as module area - the number of squares in the symbol’s grid, excluding the white quiet-zone border - so a code rendered at a larger pixel size doesn’t count as bigger. Most encoders produce the minimal symbol for most payloads; the differences show up on the harder cases. segno and qrcode produced symbols 19% larger on the mixed-mode payload, because neither switches encoding modes within a symbol. qrcode’s undeclared-UTF-8 fallback (see Correctness) was 26% larger on kanji, where it has no compact kanji mode, and 20% larger on the ambiguous Latin-1 payload. pyStrich matched the minimal symbol everywhere when the charset was pinned; its kanji mode is only available when the charset is set to Shift-JIS.
See the full report for the per-case tables, charts, exception listings and raw records.
Maintenance and maturity
The benchmark installed each library from PyPI as published. Every version tested was the latest release at the time of the run (July 2026):
| library | version tested | released | first release on PyPI | typed |
|---|---|---|---|---|
| pyStrich | 0.17 | July 2026 | July 2015 | yes, inline annotations |
| zxing-cpp | 3.1.0 | July 2026 | May 2021 | yes, stubs |
| segno | 1.6.6 | March 2025 | August 2016 | yes, stubs |
| qrcode | 8.2 | May 2025 | November 2011 | no |
| qrcodegen | 1.8.0 | April 2022 | April 2016 | no |
| opencv-python-headless | 5.0.0.93 | July 2026 | September 2018 | yes, stubs |
| treepoem | 3.28.0 | September 2025 | March 2016 | yes, inline annotations |
“Typed” means the wheel ships a PEP 561 py.typed marker, so mypy and pyright will see the library’s types - via inline annotations or via stub files. qrcode has some annotations in its code, but without the marker, type checkers ignore them.
Several of these go back further than their PyPI history suggests. pyStrich began as a fork of the older huBarcode project, zxing-cpp is a C++ port of the ZXing Java library with encoding backed by zint (a long-established C barcode library), and treepoem wraps BWIPP, a PostScript barcode library that predates most of the field.
Examples
How to use pyStrich to generate a QR code
from pystrich.qrcode import QRCodeEncoder
encoder = QRCodeEncoder("https://en.wikipedia.org/wiki/QR_code", ecl="M")
encoder.save_svg("qr.svg")
save_eps and get_dxf cover the other output formats. A plain string is encoded with the narrowest charset that fits (ASCII, Latin-1 or UTF-8, with ECI emitted where needed); to get kanji mode’s compact encoding for Japanese text, pin the charset:
from pystrich.qrcode import QRCodeData, QRCodeEncoder
QRCodeEncoder(QRCodeData("親切にしろ", encoding="shift_jis")).save("kanji.png")
Documentation: pyStrich QR code docs.
How to use zxing-cpp to generate a QR code
zxing-cpp has no PNG writer of its own; it rasterises to an array and you save it through Pillow.
import zxingcpp
from PIL import Image
barcode = zxingcpp.create_barcode(
"https://en.wikipedia.org/wiki/QR_code", zxingcpp.BarcodeFormat.QRCode, ec_level="M"
)
Image.fromarray(barcode.to_image(scale=5)).save("qr.png")
with open("qr.svg", "w", encoding="utf-8") as f:
f.write(barcode.to_svg())
One caution from building the benchmark fixture: create_barcode silently ignores unknown keyword arguments, so a misspelt option doesn’t fail - it just doesn’t take effect. Worth verifying by decoding the output if an option matters to you.
How to use segno to generate a QR code
import segno
qr = segno.make_qr("https://en.wikipedia.org/wiki/QR_code", error="m")
qr.save("qr.svg", scale=5, border=4)
One thing to know: segno silently upgrades the error correction level when the chosen symbol size has slack. That’s usually a nice free upgrade, but if you need exactly the level you asked for, pass boost_error=False.
Be careful: ECI is not on by default. Without it, non-ASCII payloads are written as bytes with no charset declaration in the symbol, leaving the reader to guess. To declare the charset explicitly, pass an encoding along with eci=True:
qr = segno.make_qr("Un chef-d'œuvre à 9,99 €", encoding="utf-8", eci=True)
qr.save("qr.png", scale=5, border=4)
Documentation: segno docs.
How to use python-qrcode to generate a QR code
import qrcode
from qrcode.constants import ERROR_CORRECT_M
from qrcode.image.svg import SvgPathImage
qr = qrcode.QRCode(error_correction=ERROR_CORRECT_M, box_size=5, border=4)
qr.add_data("https://en.wikipedia.org/wiki/QR_code")
qr.make(fit=True)
qr.make_image(image_factory=SvgPathImage).save("qr.svg")
Documentation: python-qrcode on GitHub.
How this comparison was made
A test fixture was built for each library. A corpus of examples was designed to exercise the charsets, encoding modes and payloads a QR code encoder is likely to meet in the real world - URLs, boarding passes, JSON, XML, base64, Japanese text, near-capacity payloads and so on. Each encoder was then measured generating that corpus into PNG and SVG files, inside its own container over a shared Python base, with multiple rounds per case to reduce noise from the test environment. Every output was decoded with zxing-cpp and checked against its payload; anything that didn’t round-trip was excluded from the statistics. Where a library couldn’t honour a requested option (an error correction level, a charset), the case was recorded as unsupported rather than silently reconfigured.
You can review the full report and the benchmarking suite to draw your own conclusions.
