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:

  1. Finder patterns — three large squares in the corners (top-left, top-right, bottom-left) that help scanners locate and orient the code
  2. Alignment patterns — smaller squares in larger versions that correct for surface curvature
  3. Timing patterns — alternating dark/light rows and columns between finder patterns that establish the grid spacing
  4. Format information — 15 bits near the finder patterns encoding error correction level and mask pattern
  5. Version information — 18 bits (versions 7+) encoding the version number
  6. 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:

LevelRecoveryOverheadUse Case
L~7%LowClean environments, max data
M~15%MediumGeneral purpose (default)
Q~25%HighIndustrial, moderate damage expected
H~30%HighestLogo 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_size if 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

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=10 at 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.

pythonqrcodegenerationbarcodereed-solomon

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.