You can generate a QR code in Python in three lines using the qrcode library, which renders to a Pillow image you can save or stream:
import qrcode
img = qrcode.make("https://micropyramid.com")
img.save("qr.png")
That is enough for most use cases. The rest of this guide covers the full QRCode API, choosing the right error-correction level, styling codes with rounded modules and an embedded logo, exporting SVG, serving a QR code straight from a Django or FastAPI response without touching disk, batch generation, and when to reach for segno instead.
A QR (Quick Response) code is a two-dimensional matrix barcode: black and white modules arranged in a square grid that a phone camera decodes instantly. It can hold URLs, plain text, contact cards (vCard), Wi-Fi credentials, payment links, and more.
Installing the qrcode library
The qrcode library uses Pillow as its imaging backend. Install both with the optional [pil] extra so image rendering works out of the box (Python 3.11+ recommended):
pip install "qrcode[pil]"Quick start
The qrcode.make() shortcut returns a PilImage wrapper around a Pillow image. Save it to any format Pillow supports (PNG, JPEG, GIF):
import qrcode
img = qrcode.make("https://micropyramid.com")
print(type(img)) # <class 'qrcode.image.pil.PilImage'>
img.save("basic-qr.png")The full QRCode API
For control over size, density, and error tolerance, build a QRCode instance directly instead of using the make() shortcut:
import qrcode
qr = qrcode.QRCode(
version=1, # 1-40, or None to auto-size
error_correction=qrcode.constants.ERROR_CORRECT_M,
box_size=10, # pixels per module
border=4, # quiet-zone width in modules (4 = spec minimum)
)
qr.add_data("https://micropyramid.com")
qr.make(fit=True) # auto-pick the smallest version that fits the data
img = qr.make_image(fill_color="black", back_color="white")
img.save("advanced-qr.png")What each parameter does:
version— an integer from 1 to 40 controlling the grid size. Version 1 is 21x21 modules; each step up adds 4 modules per side. Set it toNoneand callqr.make(fit=True)to let the library pick the smallest version that holds your data.error_correction— how much of the code can be damaged or obscured and still decode (see the table below).box_size— how many pixels wide each module is, i.e. the final image resolution.border— the width of the white "quiet zone" around the code, in modules. The spec minimum is 4; do not go lower or scanners may fail.
Error-correction levels
QR codes use Reed-Solomon error correction, so a code still scans even when part of it is dirty, damaged, or covered by a logo. Higher levels recover more but encode less data in the same version (the redundancy takes up space).
| Level | Constant | Recovery | When to use |
|---|---|---|---|
| L | ERROR_CORRECT_L |
~7% | Clean digital display, maximum data capacity |
| M | ERROR_CORRECT_M |
~15% | Default; good general-purpose balance |
| Q | ERROR_CORRECT_Q |
~25% | Printed codes that may get scuffed |
| H | ERROR_CORRECT_H |
~30% | Logo overlay, stickers, harsh environments |
Rule of thumb: use M for on-screen URLs, and step up to H whenever you overlay a logo or expect physical wear — the extra redundancy is what lets the code survive the obstruction.
Styled and colored QR codes
The qrcode library can render rounded modules, gradients, and an embedded logo using StyledPilImage together with module and color drawers. Always use error correction H when embedding a logo so the covered modules can be reconstructed:
import qrcode
from qrcode.image.styledpil import StyledPilImage
from qrcode.image.styles.moduledrawers.pil import RoundedModuleDrawer
from qrcode.image.styles.colormasks import RadialGradiantColorMask
qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_H)
qr.add_data("https://micropyramid.com")
qr.make(fit=True)
# Rounded modules + a radial gradient fill
img = qr.make_image(
image_factory=StyledPilImage,
module_drawer=RoundedModuleDrawer(),
color_mask=RadialGradiantColorMask(),
)
img.save("styled-qr.png")
# Embed a center logo (keep it under ~20-25% of the width)
logo_qr = qr.make_image(
image_factory=StyledPilImage,
embeded_image_path="logo.png", # note: the kwarg is spelled 'embeded'
)
logo_qr.save("logo-qr.png")Note the kwarg is embeded_image_path (the library's historical spelling). You can also pass an already-loaded Pillow image via embeded_image.
Generating an SVG QR code
When you need crisp scaling at any size — print, large displays, responsive web pages — render SVG instead of a raster PNG. SVG output needs no Pillow:
import qrcode
import qrcode.image.svg
# SvgPathImage produces one compact <path> element (smallest file)
factory = qrcode.image.svg.SvgPathImage
img = qrcode.make("https://micropyramid.com", image_factory=factory)
img.save("qr.svg")Serving a QR code from Django, Flask, or FastAPI
In a web app you rarely want to write a file to disk. Generate the QR code in memory with io.BytesIO and stream the bytes straight back in the HTTP response. Here is the same pattern for the three frameworks.
# FastAPI
import io
import qrcode
from fastapi import FastAPI
from fastapi.responses import Response
app = FastAPI()
@app.get("/qr")
def qr(data: str):
img = qrcode.make(data)
buf = io.BytesIO()
img.save(buf, format="PNG")
return Response(content=buf.getvalue(), media_type="image/png")# Django
import io
import qrcode
from django.http import HttpResponse
def qr_view(request):
data = request.GET.get("data", "")
img = qrcode.make(data)
buf = io.BytesIO()
img.save(buf, format="PNG")
return HttpResponse(buf.getvalue(), content_type="image/png")# Flask
import io
import qrcode
from flask import Flask, request, send_file
app = Flask(__name__)
@app.get("/qr")
def qr():
img = qrcode.make(request.args.get("data", ""))
buf = io.BytesIO()
img.save(buf, format="PNG")
buf.seek(0)
return send_file(buf, mimetype="image/png")Batch generation
To produce many codes at once — event tickets, asset tags, per-user links — loop over your data and save each with a unique filename:
import qrcode
urls = {
"alice": "https://example.com/u/alice",
"bob": "https://example.com/u/bob",
"carol": "https://example.com/u/carol",
}
for name, url in urls.items():
qrcode.make(url).save(f"qr-{name}.png")Alternative: segno
segno is a modern, pure-Python QR generator with no required dependencies (Pillow is optional, only for raster output). It supports Micro QR codes, animated QR (GIF/APNG), and writes SVG, PNG, EPS, PDF, and more out of the box:
pip install segnoimport segno
qr = segno.make("https://micropyramid.com", error="h")
qr.save("segno-qr.png", scale=10) # raster
qr.save("segno-qr.svg", scale=10) # vector, no extra depsQuick comparison: choose qrcode for the most common tutorials and Pillow-based styling/logo workflows; choose segno when you want zero dependencies, Micro QR, animated codes, or many output formats from a single API.
Static vs dynamic QR codes
A static QR code encodes the destination directly — the URL lives inside the pixels, so once it is printed you can never change where it points. If you might need to update the destination (a campaign page, a menu, an app-store link), encode a short redirect URL you control instead, e.g. https://yourdomain.com/r/menu. Then change the server-side redirect any time without reprinting a single code. That is all a "dynamic QR" really is: an indirection layer plus scan analytics. You can build that redirect-and-tracking layer in a few lines of Django or FastAPI — no third-party QR SaaS required.
Security aside — "quishing": QR-code phishing has risen sharply because a code hides its destination from the human eye. If you publish codes to users, prefer a branded short domain you own and consider adding a confirmation/landing step so people can see where they are going before they continue.
Frequently Asked Questions
Which error-correction level should I use?
Use M (~15% recovery) for clean on-screen URLs — it is the default and balances capacity with resilience. Step up to Q (~25%) for printed codes that may get scuffed, and to H (~30%) whenever you overlay a logo or expect physical wear. Higher levels add redundancy but reduce how much data fits in a given version.
How do I add a logo without breaking scannability?
Set error_correction=qrcode.constants.ERROR_CORRECT_H, render with StyledPilImage, and pass embeded_image_path="logo.png". Level H lets the scanner reconstruct the ~30% of modules the logo covers. Keep the logo to roughly 20-25% of the code's width and always test-scan with a real phone before shipping.
How do I serve a QR code from Django or FastAPI without saving a file?
Generate the image in memory with io.BytesIO, call img.save(buf, format="PNG"), and return the bytes in the HTTP response — Response(content=buf.getvalue(), media_type="image/png") in FastAPI or HttpResponse(buf.getvalue(), content_type="image/png") in Django. Nothing ever touches the disk.
qrcode vs segno — which library should I pick?
Use qrcode for the most common tutorials and Pillow-based styling, gradients, and logo embedding. Use segno when you want a pure-Python library with no required dependencies, Micro QR codes, animated QR, or many output formats (SVG, PNG, EPS, PDF) from one API.
How much data can a QR code hold?
A version-40 QR code at the lowest error-correction level (L) holds up to roughly 7,089 numeric digits, 4,296 alphanumeric characters, or 2,953 bytes. Real-world capacity is lower because higher error-correction levels trade data space for redundancy. For URLs, keep them short — smaller payloads mean smaller, faster-scanning codes.
What is the difference between static and dynamic QR codes?
A static QR code encodes the destination directly, so it can never be changed after printing. A dynamic QR code encodes a short redirect URL you control, letting you update the destination and capture scan analytics without reprinting. You can build that redirect layer yourself in a few lines of Django or FastAPI — no third-party service needed.
Need help building it into your product?
At MicroPyramid we have spent 12+ years and 50+ delivered projects building production Python applications — including the dynamic QR redirect, tracking, and image-generation services described above — in Django and FastAPI. If you want a robust, scannable QR feature wired into your app with proper analytics and security, our team can help.