Python QR Code Generation — Deep Dive
QR code structure
A QR code is a 2D matrix of dark and light modules arranged in a specific structure:
- Finder patterns — three large squares in the corners (top-left, top-right, bottom-left) that help scanners locate and orient the code
- Alignment patterns — smaller squares in larger versions that correct for surface curvature
- Timing patterns — alternating dark/light rows and columns between finder patterns that establish the grid spacing
- Format information — 15 bits near the finder patterns encoding error correction level and mask pattern
- Version information — 18 bits (versions 7+) encoding the version number
- Data and error correction codewords — the actual payload, filling the remaining modules in a specific zigzag pattern
The qrcode library handles all of this internally. Understanding the structure helps when debugging scanning failures or optimizing for specific use cases.
Encoding modes
QR codes support four encoding modes, each optimized for different character sets:
- Numeric — digits 0-9 only; 3 digits per 10 bits (most compact)
- Alphanumeric — digits, uppercase A-Z, and nine symbols (space, $, %, *, +, -, ., /, :); 2 characters per 11 bits
- Byte — any 8-bit data; 1 byte per 8 bits
- Kanji — Shift JIS double-byte characters; 1 character per 13 bits
The library automatically selects the most efficient mode. A URL like HTTPS://EXAMPLE.COM (uppercase) uses alphanumeric mode and fits in a smaller version than the lowercase equivalent, which requires byte mode.
You can force behavior by pre-processing data:
# Uppercase URLs use alphanumeric mode (more compact)
url = "HTTPS://EXAMPLE.COM/PATH"
qr.add_data(url)
Reed-Solomon error correction
QR codes use Reed-Solomon codes for error correction — the same algorithm used in CDs and satellite communications. The data is divided into blocks, and each block gets parity codewords appended.
The four levels and their recovery capabilities:
| Level | Recovery | Overhead | Use Case |
|---|---|---|---|
| L | ~7% | Low | Clean environments, max data |
| M | ~15% | Medium | General purpose (default) |
| Q | ~25% | High | Industrial, moderate damage expected |
| H | ~30% | Highest | Logo embedding, harsh environments |
“Recovery” means the percentage of codewords that can be damaged and still decoded. At level H, you can obscure up to 30% of the code — which is why logo embedding works.
The library selects the error correction level at construction time:
qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_H)
Masking
After placing data modules, the QR spec applies one of eight mask patterns to improve scannability. Each pattern XORs specific modules based on their row/column coordinates. The library evaluates all eight patterns against penalty rules (large same-color blocks, patterns resembling finder patterns, color imbalance) and selects the one with the lowest penalty score.
This is why two QR codes with identical data can look different — the masking algorithm may choose differently based on subtle data layout changes.
Advanced image styling
Custom module shapes
The StyledPilImage factory supports pluggable module drawers:
from qrcode.image.styledpil import StyledPilImage
from qrcode.image.styles.moduledrawers import CircleModuleDrawer
img = qr.make_image(
image_factory=StyledPilImage,
module_drawer=CircleModuleDrawer()
)
Create custom drawers by subclassing QRModuleDrawer:
from qrcode.image.styles.moduledrawers import QRModuleDrawer
from PIL import ImageDraw
class DiamondModuleDrawer(QRModuleDrawer):
def drawrect(self, box, is_active):
if not is_active:
return
x0, y0 = box[0]
x1, y1 = box[1]
cx, cy = (x0 + x1) / 2, (y0 + y1) / 2
points = [(cx, y0), (x1, cy), (cx, y1), (x0, cy)]
ImageDraw.Draw(self.img._img).polygon(points, fill=self.paint)
Color masks
Apply gradients or image-based coloring:
from qrcode.image.styles.colormasks import (
RadialGradiantColorMask,
SquareGradiantColorMask,
HorizontalGradiantColorMask,
ImageColorMask,
)
# Use a background image to color modules
img = qr.make_image(
image_factory=StyledPilImage,
color_mask=ImageColorMask(back_color=(255,255,255),
color_mask_path="gradient.png")
)
Embedded images
For professional logo embedding, use the built-in embeded_image parameter:
img = qr.make_image(
image_factory=StyledPilImage,
embeded_image_path="logo.png"
)
This automatically sizes and positions the logo. Combine with ERROR_CORRECT_H for maximum tolerance.
Batch generation
Generate QR codes at scale for inventory, events, or marketing:
import csv
import qrcode
from pathlib import Path
def generate_batch(csv_path, output_dir, template_config=None):
config = template_config or {
'version': None,
'error_correction': qrcode.constants.ERROR_CORRECT_M,
'box_size': 10,
'border': 4,
}
output = Path(output_dir)
output.mkdir(parents=True, exist_ok=True)
with open(csv_path) as f:
reader = csv.DictReader(f)
for row in reader:
qr = qrcode.QRCode(**config)
qr.add_data(row['url'])
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save(output / f"{row['id']}.png")
generate_batch("products.csv", "qr_codes/")
For thousands of codes, the bottleneck is image rendering. Optimizations:
- Use PNG format (faster than JPEG for binary images)
- Reduce
box_sizeif large resolution is unnecessary - Use SVG output for vector needs (no rasterization cost)
- Parallelize with
concurrent.futures.ThreadPoolExecutor
Dynamic QR codes
Static QR codes encode data directly. Dynamic QR codes encode a short redirect URL that you control server-side:
BASE_URL = "https://qr.example.com/r/"
def create_dynamic_qr(content_id):
redirect_url = f"{BASE_URL}{content_id}"
qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_M)
qr.add_data(redirect_url)
qr.make(fit=True)
return qr.make_image()
Benefits: the printed QR code never changes, but the destination can be updated. You can also track scan analytics (time, location, device) on the redirect server.
Validation and testing
Always validate generated codes by scanning them programmatically:
from pyzbar.pyzbar import decode
from PIL import Image
def validate_qr(image_path, expected_data):
img = Image.open(image_path)
results = decode(img)
assert len(results) == 1, f"Expected 1 QR code, found {len(results)}"
decoded = results[0].data.decode('utf-8')
assert decoded == expected_data, f"Expected '{expected_data}', got '{decoded}'"
return True
Use pyzbar (Python wrapper for zbar) or opencv-contrib-python with cv2.QRCodeDetector. Automated validation catches issues with:
- Data encoding errors
- Version too small for data (truncation)
- Colors with insufficient contrast
- Logo obstruction exceeding error correction capacity
Print considerations
For physical printing:
- Minimum size — each module should be at least 0.33mm (for reliable scanning)
- Quiet zone — maintain the 4-module border; printing to the edge of the code breaks scanning
- Contrast — dark modules must have at least 40% contrast against the background; avoid dark-on-dark or light-on-light
- Surface — glossy surfaces cause glare; matte is preferable
- Resolution — export at 300 DPI minimum for print;
box_size=10at 300 DPI gives ~0.85mm per module
Calculate print dimensions:
modules = 21 + (version - 1) * 4 # for a given version
border_modules = 4 * 2 # both sides
total_modules = modules + border_modules
dpi = 300
box_size_inches = 0.04 # ~1mm per module
image_size_px = int(total_modules * box_size_inches * dpi)
Security considerations
QR codes can encode malicious URLs, executable commands, or oversized data designed to crash scanners. When building systems that generate QR codes from user input:
- Validate and sanitize input data before encoding
- Limit maximum data length to prevent version 40 codes that are hard to scan
- For URLs, verify the domain against an allowlist
- Consider URL shortening to keep codes small and scannable
- Log generated codes for audit trails
The one thing to remember: Production QR code generation means understanding the interplay between encoding mode, error correction level, and version selection — and always validating output by programmatically scanning the generated image before deployment.
See Also
- Python Arcade Library Think of a magical art table that draws your game characters, listens when you press buttons, and cleans up the mess — that's Python Arcade.
- Python Audio Fingerprinting Ever wonder how Shazam identifies a song from just a few seconds of noisy audio? Audio fingerprinting is the magic behind it, and Python can do it too.
- Python Barcode Generation Picture the stripy labels on grocery items to understand how Python can create those machine-readable barcodes from numbers.
- Python Cellular Automata Imagine a checkerboard where each square follows simple rules to turn on or off — and suddenly complex patterns emerge like magic.
- Python Godot Gdscript Bridge Imagine speaking English to a friend who speaks French, with a translator in the middle — that's how Python talks to the Godot game engine.