Python QR Code Generator: Styled QR Codes with qrcode (2026)
QR codes are everywhere in 2026: restaurant menus, event tickets, payment links, product packaging, business cards and the Wi-Fi sign in every office. Online QR generators are easy, but many of them add tracking redirects, expire after a free trial or limit how many codes you can make. With Python you can generate unlimited, permanent QR codes in a few lines, style them with your brand colours and a logo, create hundreds of them from a spreadsheet and even check that every code really scans. This tutorial uses the popular qrcode library (version 8.2, the latest release on PyPI) with Pillow 13.0.0 for images and OpenCV 5.0 to decode the results. Everything was tested on Python 3.13.5, and every output block below is the real output of the scripts shown.
How a QR code works (in 30 seconds)
- A QR code is a grid of black and white squares called modules. The version (1 to 40) sets the grid size: version 1 is 21x21 modules and each version adds 4 modules per side.
- The three big squares in the corners are finder patterns that let a camera locate and straighten the code.
- QR codes contain Reed-Solomon error correction. Level L can recover about 7 percent of damaged data, M about 15 percent, Q about 25 percent and H about 30 percent. Higher levels make the code denser, but they are what make a logo in the middle possible.
- The border (quiet zone) around the code should be at least 4 modules wide, or some scanners fail.
Install
pip install "qrcode[pil]==8.2" opencv-python-headless
python -c "import qrcode, cv2, PIL; print(cv2.__version__, PIL.__version__)"
The [pil] extra installs Pillow, which qrcode uses to write PNG files. opencv-python-headless is only needed for the decoding step; it is the OpenCV build without GUI dependencies, which is ideal for servers and CI. Our test environment: qrcode 8.2, Pillow 13.0.0, opencv-python-headless 5.0.0.93 and NumPy 2.5.4.
Step 1: Generate basic, styled, logo, SVG and Wi-Fi QR codes
The script below covers the six things people search for most. qrcode.make() is the one-liner. The QRCode class gives full control: version=None with make(fit=True) picks the smallest version that fits your data, box_size is the number of pixels per module and border is the quiet zone. StyledPilImage adds rounded modules and colours, embedded_image_path places a logo in the centre, and SvgPathImage writes a vector file for print.
from pathlib import Path
import qrcode
from qrcode.constants import ERROR_CORRECT_H
from qrcode.image.styledpil import StyledPilImage
from qrcode.image.styles.moduledrawers.pil import RoundedModuleDrawer
from qrcode.image.styles.colormasks import SolidFillColorMask
from qrcode.image.svg import SvgPathImage
from PIL import Image, ImageDraw
out = Path("qr"); out.mkdir(exist_ok=True)
URL = "https://www.pyinns.com/python/automation/pillow-resize-crop-watermark-images-python-2026"
# 1) one-liner: PNG with default settings
qrcode.make(URL).save(out / "basic.png")
# 2) full control: version picked automatically, high error correction
qr = qrcode.QRCode(version=None, error_correction=ERROR_CORRECT_H, box_size=10, border=4)
qr.add_data(URL)
qr.make(fit=True)
print(f"version {qr.version}, {qr.modules_count}x{qr.modules_count} modules")
# 3) styled: rounded modules + brand colours
styled = qr.make_image(image_factory=StyledPilImage,
module_drawer=RoundedModuleDrawer(),
color_mask=SolidFillColorMask(back_color=(255, 255, 255),
front_color=(13, 71, 161)))
styled.save(out / "styled.png")
# 4) logo in the centre (works because ERROR_CORRECT_H can repair ~30%)
logo = Image.new("RGB", (120, 120), (255, 193, 7))
ImageDraw.Draw(logo).text((28, 40), "PY", fill=(0, 0, 0), font_size=40)
logo.save(out / "logo.png")
with_logo = qr.make_image(image_factory=StyledPilImage,
module_drawer=RoundedModuleDrawer(),
embedded_image_path=str(out / "logo.png"))
with_logo.save(out / "logo_qr.png")
# 5) SVG for print (vector, scales without blur)
qr.make_image(image_factory=SvgPathImage).save(out / "print.svg")
# 6) Wi-Fi QR: phones join the network when they scan it
wifi = "WIFI:T:WPA;S:Office-5G;P:s3cret-pass;;"
qrcode.make(wifi, error_correction=ERROR_CORRECT_H).save(out / "wifi.png")
for f in sorted(out.iterdir()):
size = Image.open(f).size if f.suffix == ".png" else "vector"
print(f"{f.name:12} {f.stat().st_size:7,} bytes {size}")
Real output
version 9, 53x53 modules
basic.png 1,118 bytes (490, 490)
logo.png 1,267 bytes (120, 120)
logo_qr.png 24,232 bytes (610, 610)
print.svg 22,479 bytes vector
styled.png 15,961 bytes (610, 610)
wifi.png 953 bytes (450, 450)
Our 88-character URL needed version 9, a 53x53 grid. With box_size=10 and a 4-module border on each side, the styled images are (53 + 8) x 10 = 610 pixels square, exactly as printed. The plain one-liner PNG is tiny (about 1 KB) because it is pure black and white, while the rounded, coloured version is about 16 KB. The SVG file is resolution-independent, so use it for posters, flyers and packaging where a PNG could look blurry.
The Wi-Fi string follows the format that both Android and iPhone cameras understand: WIFI:T:WPA;S:network-name;P:password;;. Use T:nopass for an open network, and escape the characters ; , : \ and " with a backslash if they appear in the name or password.
Step 2: Verify that every QR code scans, then batch-generate from a CSV
Never print 500 tickets without testing them. OpenCV's QRCodeDetector reads a QR code from an image and returns the decoded text, so we can check each file automatically. The second half of the script reads a CSV file and creates one QR code per row, which is the pattern for event tickets, asset tags, product labels and employee badges.
import csv
from pathlib import Path
import cv2
import qrcode
from qrcode.constants import ERROR_CORRECT_M
detector = cv2.QRCodeDetector()
def decode(path: Path) -> str:
img = cv2.imread(str(path))
text, points, _ = detector.detectAndDecode(img)
return text or "NOT DECODED"
# 1) check that every generated code really scans
for name in ["basic.png", "styled.png", "logo_qr.png", "wifi.png"]:
text = decode(Path("qr") / name)
print(f"{name:12} -> {text[:40]}{'...' if len(text) > 40 else ''}")
# 2) batch: one QR code per row of a CSV file (e.g. event tickets)
rows = [("T-1001", "Ayesha Khan"), ("T-1002", "John Smith"), ("T-1003", "Maria Garcia")]
with open("tickets.csv", "w", newline="") as f:
csv.writer(f).writerows([("ticket_id", "name"), *rows])
batch = Path("tickets"); batch.mkdir(exist_ok=True)
with open("tickets.csv", newline="") as f:
for row in csv.DictReader(f):
payload = f"TICKET:{row['ticket_id']}|{row['name']}"
qrcode.make(payload, error_correction=ERROR_CORRECT_M, box_size=8).save(batch / f"{row['ticket_id']}.png")
ok = sum(decode(p).startswith("TICKET:") for p in sorted(batch.glob("*.png")))
print(f"batch: {ok}/{len(rows)} ticket QR codes decoded OK")
print("sample:", decode(batch / "T-1002.png"))
Real output (verify and batch)
basic.png -> https://www.pyinns.com/python/automation...
styled.png -> https://www.pyinns.com/python/automation...
logo_qr.png -> https://www.pyinns.com/python/automation...
wifi.png -> WIFI:T:WPA;S:Office-5G;P:s3cret-pass;;
batch: 3/3 ticket QR codes decoded OK
sample: TICKET:T-1002|John Smith
All four images from Step 1 decode back to the original text, including the version with the yellow logo covering the centre. That only works because we used ERROR_CORRECT_H: the logo destroys some modules and error correction rebuilds them. With level L the same logo would usually make the code unreadable. All three ticket codes in the batch also decoded correctly.
Best practices for QR codes that always scan
- Keep contrast high. Dark modules on a light background work best. Inverted (light on dark) codes and pale colours fail on many older phones.
- Use error correction H with a logo, and keep the logo under about 20 to 25 percent of the code area.
- Keep the data short. A shorter URL gives a smaller version, bigger modules and faster scanning. Use your own short path instead of long tracking parameters.
- Never remove the quiet zone. Keep
border=4or more. - Size for distance. A common rule is that the printed code should be at least one tenth of the scanning distance, so about 2 cm for 20 cm and 10 cm for a poster read from 1 metre.
- Test before printing. Run the OpenCV check in your build script and also scan a few codes with real phones.
- Do not encode secrets you would not show. Anyone who photographs a Wi-Fi QR code can read the password.
Where to use this in your automation
- Add a QR code to product photos or social images with Pillow (resize, crop and watermark) by pasting the PNG onto the picture.
- Put ticket or invoice QR codes into PDF documents and merge them with pypdf.
- Schedule the CSV batch to run every night as part of the workflows in our Python automation mastery guide.
Conclusion
With qrcode 8.2 and Pillow 13 on Python 3.13 you can generate plain, styled, logo, SVG and Wi-Fi QR codes in a few lines, create a whole batch from a CSV file and use OpenCV to prove that every code scans before it reaches the printer. No online generator, no expiry and no tracking redirects: the codes are yours forever.