How to Build a Python QR Code Generator (qrcode Library)

A coffee shop client asked me for printed QR codes on every table last month, one for the menu and one for guest Wi-Fi. A Python QR code generator built on the qrcode library handled it: qrcode.make(data) returns the image, and .save() writes the PNG.

Free online generators looked quicker, but two of them hid the link behind a short URL that dies when the trial ends. Generate the image yourself and the real URL’s inside the code.

Quick answer: install the library with Pillow support, then make and save the image:

pip install "qrcode[pil]"

import qrcode
qrcode.make("https://example.com/harbor-lane/menu").save("menu_qr.png")

In this tutorial, I’ll build a Python QR code generator for that shop, Harbor Lane Coffee in Portland, Maine: colors, error correction, SVG, a logo, Wi-Fi and vCard codes, a CSV batch, a Tkinter app and an OpenCV scan test.

All the output on this page comes from real runs on Python 3.12.5, qrcode 8.2, Pillow 12.3.0 and OpenCV 5.0.0.

Make your first QR code with qrcode.make()

qrcode.make() takes any string and picks sensible defaults. Here it is on the shop’s menu link:

import qrcode

img = qrcode.make("https://example.com/harbor-lane/menu")
img.save("menu_qr.png")
print(type(img).__name__, img.pixel_size, "px")

Output:

PilImage 370 px
QR code PNG generated by Python qrcode.make() for the Harbor Lane Coffee menu URL
The real menu_qr.png file: 370 x 370 pixels with the default 10-pixel boxes.

.save() works like Pillow’s: the extension picks the format. My guide on saving an image to a file in Python covers JPG, WebP and folders.

Control the version, box size, border and colors with QRCode

The QRCode class gives you the knobs. version (1-40) sets the grid size, box_size is pixels per module (one black or white square), and border is the blank margin, 4 modules by default:

import qrcode
from qrcode.constants import ERROR_CORRECT_M
from qrcode.exceptions import DataOverflowError

url = "https://example.com/harbor-lane/menu"

qr = qrcode.QRCode(version=None, error_correction=ERROR_CORRECT_M,
                   box_size=10, border=4)
qr.add_data(url)
qr.make(fit=True)                  # smallest version that fits
img = qr.make_image(fill_color="#1f3a5f", back_color="#fdf6e3")
img.save("menu_brand.png")
print("version:", qr.version, "| modules:", qr.modules_count,
      "| pixels:", img.pixel_size)

tiny = qrcode.QRCode(version=1)
tiny.add_data(url)
try:
    tiny.make(fit=False)           # force version 1
except DataOverflowError as err:
    print("DataOverflowError:", err)

Output:

version: 3 | modules: 29 | pixels: 370
DataOverflowError: Code length overflow. Data size (300) > size available (128)
Command Prompt output of the Python qrcode QRCode class picking version 3 with fit=True and raising DataOverflowError for version 1
fit=True chose version 3; forcing version 1 raised DataOverflowError because the URL needs more bits than it holds.
Brand-colored QR code made in Python with qrcode make_image fill_color and back_color
The menu_brand.png output: navy modules on a cream background.

Leave version=None with fit=True unless a print layout needs a fixed grid. Keep the fill much darker than the background; light-on-dark codes don’t scan in many camera apps.

Which QR code error correction level should you use?

Error correction lets a scanner rebuild damaged parts: L recovers about 7%, M 15% (the default), Q 25% and H 30%. More recovery needs more modules, as the same URL shows:

import qrcode
from qrcode import constants

url = "https://example.com/harbor-lane/menu"
for level in "LMQH":
    ec = getattr(constants, f"ERROR_CORRECT_{level}")
    qr = qrcode.QRCode(error_correction=ec)
    qr.add_data(url)
    qr.make(fit=True)
    n = qr.modules_count
    print(f"{level}: version {qr.version}, {n} x {n} modules")

Output:

L: version 3, 29 x 29 modules
M: version 3, 29 x 29 modules
Q: version 4, 33 x 33 modules
H: version 5, 37 x 37 modules
Command Prompt output of Python qrcode error correction levels L, M, Q and H with the version and module count for each
L and M both fit the menu URL in version 3; Q needs version 4 and H version 5.

I use M for screens and print, and H when a logo covers the middle or the code goes on a cup.

Save a QR code as an SVG file

For resized signs, pass an SVG image factory:

import qrcode
from qrcode.image.svg import SvgPathImage

url = "https://example.com/harbor-lane/menu"
img = qrcode.make(url, image_factory=SvgPathImage)
img.save("menu_qr.svg")

with open("menu_qr.svg", encoding="utf-8") as f:
    svg = f.read()
print(len(svg), "characters,", svg.count("<path"), "path element")

Output:

6615 characters, 1 path element

SvgPathImage draws the code as one path, so the file stays small. You can paste it into HTML or turn the SVG into a React component.

Add a logo and rounded modules with StyledPilImage

StyledPilImage adds module shapes, color gradients and an embedded image. My logo.png is a 200 x 200 square with the shop’s initials. The library only allows a logo at level H:

import cv2
import qrcode
from qrcode.constants import ERROR_CORRECT_H
from qrcode.image.styledpil import StyledPilImage
from qrcode.image.styles.colormasks import RadialGradiantColorMask
from qrcode.image.styles.moduledrawers.pil import RoundedModuleDrawer

qr = qrcode.QRCode(error_correction=ERROR_CORRECT_H, box_size=10)
qr.add_data("https://example.com/harbor-lane/menu")
qr.make(fit=True)
img = qr.make_image(
    image_factory=StyledPilImage,
    module_drawer=RoundedModuleDrawer(),
    color_mask=RadialGradiantColorMask(
        back_color=(255, 255, 255), center_color=(154, 52, 18),
        edge_color=(31, 58, 95)),
    embedded_image_path="logo.png",
)
img.save("menu_logo.png")

text, points, _ = cv2.QRCodeDetector().detectAndDecode(
    cv2.imread("menu_logo.png"))
print("version", qr.version, "| decoded:", text)

Output:

version 5 | decoded: https://example.com/harbor-lane/menu
Python QR code with a logo in the center, rounded modules and a radial gradient made with qrcode StyledPilImage
The styled menu_logo.png; OpenCV still decodes the full menu URL from it.

By default the logo takes a quarter of the width (embedded_image_ratio=0.25). Keep your logo small; compressing images with Python helps when a brand PNG is several MB.

Encode Wi-Fi details and a vCard contact

A QR code is just text; the phone acts on its format. Wi-Fi uses WIFI:T:WPA;S:name;P:password;;, and special characters need a backslash:

import cv2
import qrcode

def wifi_payload(ssid, password, auth="WPA"):
    esc = lambda s: "".join("\\" + c if c in '\\;,":' else c for c in s)
    return f"WIFI:T:{auth};S:{esc(ssid)};P:{esc(password)};;"

wifi = wifi_payload("HarborLane-Guest", "Cold;Brew:26")
vcard = "\n".join([
    "BEGIN:VCARD", "VERSION:3.0",
    "N:Turner;Rachel", "FN:Rachel Turner",
    "TEL;TYPE=WORK:+1-207-555-0148",
    "EMAIL:rachel@example.com", "END:VCARD",
])

detector = cv2.QRCodeDetector()
for name, data in (("wifi", wifi), ("vcard", vcard)):
    qrcode.make(data, box_size=8).save(f"{name}.png")
    text, _, _ = detector.detectAndDecode(cv2.imread(f"{name}.png"))
    print(f"--- {name}.png scans back exactly: {text == data}")
    print(text)

Output:

--- wifi.png scans back exactly: True
WIFI:T:WPA;S:HarborLane-Guest;P:Cold\;Brew\:26;;
--- vcard.png scans back exactly: True
BEGIN:VCARD
VERSION:3.0
N:Turner;Rachel
FN:Rachel Turner
TEL;TYPE=WORK:+1-207-555-0148
EMAIL:rachel@example.com
END:VCARD
Command Prompt output of Python generating Wi-Fi and vCard QR codes and decoding them back with OpenCV QRCodeDetector
Both codes scan back to exactly what went in, escaped password included.

Phones offer to join the network or save the contact. If you only need one business card code and no script, the free QR code generator for digital business cards on this site does it in the browser.

Generate QR codes in bulk from a CSV file

Same menu URL pattern, now one code per store. The store list lives in a small CSV (see creating a CSV file in Python), and every file gets decoded right after it’s saved:

city,slug
Portland ME,portland
Burlington VT,burlington
Salem MA,salem
import csv
from pathlib import Path
import cv2
import qrcode

out = Path("store_qr")
out.mkdir(exist_ok=True)
detector = cv2.QRCodeDetector()

with open("stores.csv", newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        url = f"https://example.com/harbor-lane/{row['slug']}/menu"
        path = out / f"menu_{row['slug']}.png"
        qrcode.make(url).save(path)
        text, _, _ = detector.detectAndDecode(cv2.imread(str(path)))
        status = "scans OK" if text == url else "FAILED"
        print(f"{row['city']:<14} {path.as_posix():<30} {status}")

Output:

Portland ME    store_qr/menu_portland.png     scans OK
Burlington VT  store_qr/menu_burlington.png   scans OK
Salem MA       store_qr/menu_salem.png        scans OK