QR Code Decoder Python Tutorial — Read Any QR or Barcode from an Image
QR codes are everywhere — product labels, boarding passes, WiFi stickers, payment terminals. If you're building an automation, a data extraction pipeline, or just a one-off script, Python has several solid libraries for decoding them.
This tutorial covers:
- Decoding from a static image file (the common case)
- Webcam scanning in real-time with OpenCV
- Parsing WiFi QR codes into usable fields
- Batch processing a folder of images
- Which library to use and when
Library Overview
There are three Python libraries worth knowing:
| Library | Formats | Install complexity | Best for |
|---|---|---|---|
| pyzbar | QR, Code 128, EAN, UPC, PDF417 | Requires libzbar system lib | Fast, battle-tested QR + 1D barcodes |
| zxingcpp | QR, PDF417, Data Matrix, Aztec, Code 128, EAN, UPC, and more | pip install only | Broadest format support, easiest cross-platform setup |
| opencv-python | QR only (built-in detector) | pip install only | When you're already using OpenCV |
For most projects, zxingcpp is the pragmatic choice: no system dependencies, wide format support, and good accuracy on difficult images. pyzbar is faster to initialize for 1D barcodes. OpenCV's built-in decoder is convenient if you're already in an OpenCV context but only covers QR.
Installation
pyzbar
# Linux
sudo apt-get install libzbar0
pip install pyzbar Pillow
# Mac
brew install zbar
pip install pyzbar Pillow
# Windows — install DLLs manually, then:
pip install pyzbar Pillow
zxingcpp
pip install zxingcpp Pillow
No system libraries needed. Works on Linux, Mac, and Windows.
OpenCV (for webcam scanning)
pip install opencv-python
Decode a QR Code from an Image File
Using pyzbar
from PIL import Image
from pyzbar.pyzbar import decode
def decode_qr(image_path: str) -> list[str]:
img = Image.open(image_path)
results = decode(img)
return [result.data.decode("utf-8") for result in results]
codes = decode_qr("qrcode.png")
for code in codes:
print(code)
decode() returns a list — one entry per code found in the image. Each entry has .data (bytes), .type (e.g. QRCODE, CODE128), and bounding box coordinates.
Using zxingcpp
import zxingcpp
from PIL import Image
def decode_qr(image_path: str) -> list[str]:
img = Image.open(image_path).convert("RGB")
results = zxingcpp.read_barcodes(img)
return [result.text for result in results]
codes = decode_qr("qrcode.png")
for code in codes:
print(code)
zxingcpp's read_barcodes() accepts a Pillow image or a NumPy array. Each result has .text, .format (e.g. QRCode, PDF417), and position data.
Webcam Scanning with OpenCV
This loop captures frames from your webcam and decodes any QR code it finds:
import cv2
import zxingcpp
cap = cv2.VideoCapture(0) # 0 = default webcam
print("Scanning — press Q to quit")
while True:
ret, frame = cap.read()
if not ret:
break
results = zxingcpp.read_barcodes(frame)
for result in results:
print(f"[{result.format}] {result.text}")
# Draw bounding box
pts = result.position
cv2.polylines(frame, [pts.to_numpy()], True, (0, 255, 0), 2)
cv2.imshow("QR Scanner", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cap.release()
cv2.destroyAllWindows()
result.position.to_numpy() gives you the four corner points of the detected code so you can draw the bounding box on the preview frame.
Parsing WiFi QR Codes
WiFi QR codes store credentials in a specific format defined in the mecard spec:
WIFI:T:WPA2;S:MyNetwork;P:mypassword123;;
Standard decoders return this raw string. To extract the individual fields:
import re
def parse_wifi_qr(raw: str) -> dict | None:
if not raw.startswith("WIFI:"):
return None
fields = {}
# Each field is KEY:VALUE; — handle escaped semicolons
pattern = re.compile(r'([STPH]):([^;]*?)(?:;|$)')
for match in pattern.finditer(raw[5:]): # skip "WIFI:"
key, value = match.group(1), match.group(2)
fields[key] = value.replace("\\;", ";").replace("\\\\", "\\").replace("\\\"", "\"")
return {
"ssid": fields.get("S", ""),
"password": fields.get("P", ""),
"security": fields.get("T", ""),
"hidden": fields.get("H", "false").lower() == "true",
}
# Example
raw = decode_qr("wifi_sticker.png")[0]
wifi = parse_wifi_qr(raw)
if wifi:
print(f"SSID: {wifi['ssid']}")
print(f"Password: {wifi['password']}")
print(f"Security: {wifi['security']}")
The P field can be empty for open networks. The backslash escaping handles passwords with special characters like ;, ", or \.
Batch Processing a Folder
from pathlib import Path
from PIL import Image
import zxingcpp
def batch_decode(folder: str) -> dict[str, list[str]]:
results = {}
extensions = {".png", ".jpg", ".jpeg", ".webp", ".gif"}
for path in Path(folder).iterdir():
if path.suffix.lower() not in extensions:
continue
try:
img = Image.open(path).convert("RGB")
codes = zxingcpp.read_barcodes(img)
results[path.name] = [c.text for c in codes]
except Exception as e:
results[path.name] = [f"ERROR: {e}"]
return results
for filename, codes in batch_decode("./images").items():
if codes:
print(f"{filename}: {codes}")
else:
print(f"{filename}: no code found")
For large batches, use concurrent.futures.ThreadPoolExecutor to parallelize the image opens — the zxingcpp decoding itself releases the GIL.
Improving Decode Accuracy on Difficult Images
If a clean decode isn't working on a real-world image (faded sticker, photo at an angle, low resolution):
import cv2
import numpy as np
import zxingcpp
from PIL import Image
def decode_with_preprocessing(image_path: str) -> list[str]:
img = cv2.imread(image_path)
# Convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Increase contrast
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
enhanced = clahe.apply(gray)
# Convert back to PIL for zxingcpp
pil_img = Image.fromarray(enhanced).convert("RGB")
results = zxingcpp.read_barcodes(pil_img)
return [r.text for r in results]
Common preprocessing that helps:
- Grayscale — removes color noise that confuses edge detection
- CLAHE — adaptive histogram equalization, improves contrast in locally dark/bright areas
- Thresholding —
cv2.adaptiveThresholdcan help with inconsistent lighting - Upscaling — if the image is small, resize to 2× before decoding (
cv2.resize)
Choosing Between pyzbar and zxingcpp
Use pyzbar when:
- You only need QR codes and 1D barcodes (Code 128, EAN, UPC)
- You're on Linux and libzbar is already available
- You need the bounding box polygon for visual annotation and prefer pyzbar's output format
Use zxingcpp when:
- You need PDF417, Data Matrix, Aztec, or other 2D formats
- You want a cross-platform install with no system dependencies
- You're building a CI pipeline or Docker image where you can't easily install system libs
Both libraries can be combined: try zxingcpp first, fall back to pyzbar if no result:
def decode_any(image_path: str) -> list[str]:
from PIL import Image
import zxingcpp
from pyzbar.pyzbar import decode as pyzbar_decode
img = Image.open(image_path).convert("RGB")
results = zxingcpp.read_barcodes(img)
if results:
return [r.text for r in results]
# Fallback
results = pyzbar_decode(img)
return [r.data.decode("utf-8") for r in results]
This dual-engine approach mirrors what WifiQRScan does in the browser — two libraries in sequence to maximize coverage.
Full Example Script
#!/usr/bin/env python3
"""
Decode any QR code or barcode from an image file.
Usage: python decode_qr.py <image_path>
"""
import sys
import re
from PIL import Image
import zxingcpp
def parse_wifi(raw: str) -> dict | None:
if not raw.startswith("WIFI:"):
return None
pattern = re.compile(r'([STPH]):([^;]*?)(?:;|$)')
fields = {m.group(1): m.group(2) for m in pattern.finditer(raw[5:])}
return {
"ssid": fields.get("S", ""),
"password": fields.get("P", ""),
"security": fields.get("T", ""),
}
def main(image_path: str) -> None:
img = Image.open(image_path).convert("RGB")
results = zxingcpp.read_barcodes(img)
if not results:
print("No barcode or QR code found.")
return
for result in results:
print(f"Format: {result.format}")
print(f"Raw: {result.text}")
wifi = parse_wifi(result.text)
if wifi:
print(f" SSID: {wifi['ssid']}")
print(f" Password: {wifi['password']}")
print(f" Security: {wifi['security']}")
print()
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python decode_qr.py <image_path>")
sys.exit(1)
main(sys.argv[1])
Save it as decode_qr.py and run:
python decode_qr.py router_sticker.jpg
Online Alternative
If you don't want to run Python locally, WifiQRScan's decode page does the same thing in your browser — paste a screenshot, upload a file, or point a webcam. It uses ZXing compiled to WebAssembly, so the decoding is local and your image never leaves your device.
Try it free — no app, no account
No Python required — paste, upload, or use your webcam. Powered by ZXing-wasm.