Down the QR Rabbit Hole — A Weekend with Python & Pixels
Table of Contents
- A Little History: The Inventor Nobody Remembers
- What a QR Code Actually Is
- Anatomy of a QR Code
- Versions, Sizes & Error Correction
- How Data Gets Encoded — The Process Under the Hood
- Generating QR Codes with Python
- Installation
- Basic Usage — The Lazy Way
- Fine-Grained Control
- Batch Generation — QR Codes for a List of URLs
- Styled QR Code with a Custom Color Scheme
- Real-World Use Cases Worth Knowing
- The Takeaway
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 Type | Maximum Capacity |
|---|---|
| Numeric only | 7,089 digits |
| Alphanumeric | 4,296 characters |
| Binary bytes | 2,953 bytes |
| Kanji | 1,817 characters |
Anatomy of a QR Code
Every QR code — no matter what it encodes — contains the same structural components:
| Component | Description |
|---|---|
| Finder Patterns | Three 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 Patterns | Smaller 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 Patterns | Alternating black-and-white strips running between the finder patterns. They establish the coordinate system — like a ruler baked into the code itself. |
| Format Information | Strips adjacent to the finder patterns encoding the error correction level and mask pattern used. Stored twice for redundancy. |
| Data & Error Correction | The bulk of the QR code. Your actual payload is encoded here using Reed-Solomon error correction codewords. |
| Quiet Zone | A 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.
| Version | Grid Size | Max Alphanumeric | Common Use |
|---|---|---|---|
| 1 | 21×21 | 25 chars | Very short codes, labels |
| 3 | 29×29 | 77 chars | Short URLs |
| 5 | 37×37 | 154 chars | Contact info, short links |
| 10 | 57×57 | 395 chars | Long URLs, vCards |
| 25 | 117×117 | 1,367 chars | Dense data payloads |
| 40 | 177×177 | 4,296 chars | Maximum 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:
| Level | Code | Recovery Capacity | Trade-off |
|---|---|---|---|
| Low | L | ~7% can be restored | Smallest size, fragile |
| Medium | M | ~15% can be restored | Good default |
| Quartile | Q | ~25% can be restored | Recommended for logos |
| High | H | ~30% can be restored | Largest, 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.01URI 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.