QR Code Decoder Python Tutorial — Read Any QR or Barcode from an Image

7 min read
pythonqr-code-decoderzxingguidedeveloper
In this guide:

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:

LibraryFormatsInstall complexityBest for
pyzbarQR, Code 128, EAN, UPC, PDF417Requires libzbar system libFast, battle-tested QR + 1D barcodes
zxingcppQR, PDF417, Data Matrix, Aztec, Code 128, EAN, UPC, and morepip install onlyBroadest format support, easiest cross-platform setup
opencv-pythonQR only (built-in detector)pip install onlyWhen 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
  • Thresholdingcv2.adaptiveThreshold can 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.

Decode a QR Code in Your Browser

Frequently Asked Questions

What is the best Python library for decoding QR codes?
pyzbar is the most widely used for QR and 1D barcodes, but it requires the libzbar system library. zxingcpp is a better choice if you need PDF417, Data Matrix, Aztec, or other 2D formats — it's pure C++ bindings with no external system dependency. For simple QR-only use, the qrcode package combined with Pillow works well.
Can Python decode QR codes from a webcam?
Yes. OpenCV (cv2) captures webcam frames and pyzbar or zxingcpp decodes them. The loop is under 10 lines of code — see the webcam section of this guide for the complete example.
How do I decode a WiFi QR code in Python?
Use pyzbar or zxingcpp to extract the raw string, which looks like WIFI:T:WPA2;S:NetworkName;P:password;;. Then split or use a regex to extract each field. The guide below includes a parse_wifi_qr() function you can copy directly.
Does pyzbar work on Windows?
Yes, but you need to install the libzbar DLLs separately on Windows. The easiest path is pip install pyzbar[dev] or downloading the DLLs manually and adding them to your PATH. zxingcpp is often easier to install cross-platform — just pip install zxingcpp, no system libraries needed.
What barcode formats does zxingcpp support?
zxingcpp supports QR Code, Data Matrix, Aztec, PDF417, Code 128, Code 39, Code 93, EAN-13, EAN-8, UPC-A, UPC-E, ITF, Codabar, and more. It's based on the ZXing C++ library, which is the most comprehensive open-source barcode library available.