· 9 min read

Down the QR Rabbit Hole — A Weekend with Python & Pixels


Table of Contents

I never thought much about QR codes — they were just those black-and-white squares I pointed my phone at. Then yesterday happened. I opened a Python terminal, typed four lines, and generated one myself. That was enough to make me want to understand everything.


A Little History: The Inventor Nobody Remembers

QR codes were invented in 1994 by Masahiro Hara and his team at Denso Wave, a Toyota subsidiary in Japan. At the time, automotive assembly lines were drowning in paperwork — each car component needed a barcode scan, but the old 1D barcodes (the zebra stripes you see on grocery items) could only hold about 20 alphanumeric characters. Tracking complex part numbers was a logistical nightmare.

Hara’s insight was to go two-dimensional. Legend has it he was playing the board game Go one afternoon and noticed how the black-and-white stones on a grid could encode vast positional information. The analogy stuck, and the QR (Quick Response) code was born.

Fun Fact

Denso Wave deliberately chose not to enforce their QR code patent, releasing it as an open standard. That single decision is arguably what made QR codes ubiquitous worldwide. No licensing fees, no gatekeepers.

For years, QR codes remained niche — mostly used in Japanese manufacturing and logistics. Their mainstream breakthrough in the West came slowly, aided by smartphone cameras, and then dramatically accelerated in 2020 when contactless everything became essential. COVID menus, vaccine passes, payment terminals — QR codes were suddenly everywhere.


What a QR Code Actually Is

A QR code is a 2D matrix barcode — a grid of black and white squares (called modules) that encodes data using spatial patterns. Unlike a 1D barcode that only varies horizontally, a QR code carries information in both dimensions, which is why it can hold so much more.

Data TypeMaximum Capacity
Numeric only7,089 digits
Alphanumeric4,296 characters
Binary bytes2,953 bytes
Kanji1,817 characters

Anatomy of a QR Code

Every QR code — no matter what it encodes — contains the same structural components:

ComponentDescription
Finder PatternsThree identical square patterns in the corners (top-left, top-right, bottom-left). Their 1:1:3:1:1 dark-light ratio lets scanners detect the code at any angle or rotation.
Alignment PatternsSmaller squares placed within the data area (for Version 2+). They help correct for distortion when the code is on a curved surface or photographed at an angle.
Timing PatternsAlternating black-and-white strips running between the finder patterns. They establish the coordinate system — like a ruler baked into the code itself.
Format InformationStrips adjacent to the finder patterns encoding the error correction level and mask pattern used. Stored twice for redundancy.
Data & Error CorrectionThe bulk of the QR code. Your actual payload is encoded here using Reed-Solomon error correction codewords.
Quiet ZoneA mandatory 4-module-wide white border around the entire code. Without it, scanners can’t reliably find the edges against a background.

Versions, Sizes & Error Correction

QR codes come in 40 versions, ranging from Version 1 (21×21 modules) to Version 40 (177×177 modules). Each version step adds 4 modules to each side. More modules = more data capacity.

VersionGrid SizeMax AlphanumericCommon Use
121×2125 charsVery short codes, labels
329×2977 charsShort URLs
537×37154 charsContact info, short links
1057×57395 charsLong URLs, vCards
25117×1171,367 charsDense data payloads
40177×1774,296 charsMaximum capacity

One of the most underrated features of QR codes is error correction. Using Reed-Solomon error correction, a QR code can still be decoded even if part of it is obscured or damaged. There are four levels:

LevelCodeRecovery CapacityTrade-off
LowL~7% can be restoredSmallest size, fragile
MediumM~15% can be restoredGood default
QuartileQ~25% can be restoredRecommended for logos
HighH~30% can be restoredLargest, most robust

This is why you can put a logo in the center of a QR code and it still scans — the logo deliberately destroys some modules, and the error correction fills them back in. As long as less than 30% of the code is damaged (with level H), decoding succeeds.


How Data Gets Encoded — The Process Under the Hood

When you give a QR encoder a string like "https://example.com", here’s roughly what happens before a single pixel is drawn:

1. Mode selection. The encoder inspects the data and picks the most efficient encoding mode. Purely numeric data uses a 10-bit-per-3-digit scheme. Alphanumeric (A–Z, 0–9, a handful of symbols) uses 11 bits per 2 characters. Binary/byte mode handles arbitrary UTF-8 at 8 bits per character.

2. Data encoding. The characters are converted to binary bit streams per the chosen mode. For example, in alphanumeric mode, “AC” becomes a number (10×45 + 12 = 462) encoded in 11 bits.

3. Error correction codewords. The data bits are divided into blocks, and Reed-Solomon codewords are computed for each block. These extra codewords carry enough redundancy to reconstruct damaged data.

4. Interleaving & final bit stream. Data and error correction blocks from different codeword sequences are interleaved — this spreads burst errors across multiple blocks, improving resilience.

5. Module placement. The bit stream is mapped onto the grid in a specific zigzag pattern, skipping functional regions (finder patterns, timing strips, etc.).

6. Masking. A mask pattern (one of 8 predefined XOR patterns) is applied to the data modules to ensure no large uniform regions exist — such regions confuse scanners. The encoder tries all 8 masks and picks the one with the best “penalty score.”


Generating QR Codes with Python

Okay — theory mode off. Here’s what actually got me down this rabbit hole. I installed the qrcode library and had a working QR code in under two minutes.

Installation

# Install the library (Pillow handles image rendering)
pip install qrcode[pil]

Basic Usage — The Lazy Way

import qrcode

# One-liner: generate and save
img = qrcode.make("https://example.com")
img.save("my_first_qr.png")

That’s it. A 290×290 PNG with a perfectly valid QR code. I was almost disappointed by how easy it was — until I started digging into the configuration options.

Fine-Grained Control

import qrcode
from qrcode.constants import ERROR_CORRECT_H

qr = qrcode.QRCode(
    version=None,                      # None = auto-select smallest version that fits
    error_correction=ERROR_CORRECT_H,  # 30% damage recovery
    box_size=10,                       # pixels per module
    border=4,                          # quiet zone in modules (min = 4)
)

qr.add_data("https://example.com/super-long-url?with=parameters")
qr.make(fit=True)          # recalculate version to fit the data

img = qr.make_image(
    fill_color="black",
    back_color="white"
)
img.save("custom_qr.png")

# Pro tip: inspect what version was auto-selected
print(f"Version: {qr.version}")        # e.g. Version: 3
print(f"Grid size: {qr.modules_count}") # e.g. Grid size: 29

Batch Generation — QR Codes for a List of URLs

import qrcode
from pathlib import Path

urls = [
    ("home",    "https://example.com"),
    ("docs",    "https://docs.example.com"),
    ("contact", "https://example.com/contact"),
]

output_dir = Path("qr_codes")
output_dir.mkdir(exist_ok=True)

for name, url in urls:
    img = qrcode.make(url)
    img.save(output_dir / f"{name}.png")
    print(f"Generated {name}.png")

Styled QR Code with a Custom Color Scheme

import qrcode
from qrcode.image.styledpil import StyledPilImage
from qrcode.image.styles.moduledrawers.pil import RoundedModuleDrawer

qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_H)
qr.add_data("https://example.com")
qr.make(fit=True)

img = qr.make_image(
    image_factory=StyledPilImage,
    module_drawer=RoundedModuleDrawer(),   # rounded modules
    color_mask=qrcode.image.styles.colormasks.SolidFillColorMask(
        front_color=(0, 82, 204),    # deep blue modules
        back_color=(255, 255, 255)   # white background
    )
)
img.save("styled_qr.png")

Practical Warning

Styled and colored QR codes look great but test them on multiple devices before publishing. Low contrast ratios (e.g., light blue on white) can fail on older scanner apps. Rule of thumb: always ensure a dark-on-light contrast ratio of at least 4:1.


Real-World Use Cases Worth Knowing

What surprised me most after this deep-dive is the range of use cases QR codes actually handle well:

  • WiFi credentials — The WIFI:T:WPA;S:MyNetwork;P:MyPassword;; format lets phones join a network without typing a password. Useful for office guest networks.

  • vCards / contact sharing — A QR code encoding a vCard 3.0 payload lets someone scan and instantly add you to their contacts. No app required.

  • Cryptocurrency addresses — Every crypto wallet uses QR codes for addresses. Instead of typing a 42-character hex string, you scan. The QR code may also encode the amount and memo using the bitcoin:?amount=0.01 URI scheme.

  • TOTP / 2FA setup — Authenticator apps (Google Authenticator, Authy) use QR codes to import TOTP secrets. The payload follows the otpauth:// URI scheme.

  • Event ticketing — A signed QR code payload can encode a ticket ID plus an HMAC signature. The scanner verifies the signature — no internet needed for validation.


The Takeaway

What started as a throwaway afternoon experiment turned into a genuine appreciation for an elegantly designed standard. QR codes are thirty years old, patent-free, work offline, require no special hardware beyond a camera, carry error correction baked in, and are readable from any rotation. Honestly, that’s a remarkable engineering achievement.

The Python qrcode library makes generation trivially easy, but now I understand why those four lines of code produce something a phone can decode reliably — the mathematics of Reed-Solomon, the geometry of finder patterns, and the deliberate choice to keep the spec open.

Next experiment on my list: decode a QR code from scratch, pixel by pixel, without any library. Wish me luck.