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

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)

fit=True chose version 3; forcing version 1 raised DataOverflowError because the URL needs more bits than it holds.
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

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

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

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
