Preparing your learning space...
91% through Mini Projects tutorials
A program that generates QR code images from user-provided text or URLs. You will learn how to use the qrcode library, customize QR code appearance (size, colors, error correction), save images to disk, and build a batch generator that processes multiple inputs from a file. QR codes are everywhere — product packaging, restaurant menus, event tickets, business cards.
QR codes are two-dimensional barcodes that smartphones can scan to open websites, display text, or connect to Wi-Fi networks. This program takes any text or URL, generates a QR code image using the qrcode library (which uses Python Imaging Library / Pillow under the hood), and saves it as a PNG file. The user can customize the size, colors, and error correction level.
Where this is used in the real world: QR codes are used in contactless payments, restaurant menus, product packaging, event ticketing, museum exhibits, hotel check-ins, and digital business cards.
Knowledge needed
open(), readlines())for loopsSoftware needed
qrcode library (pip install qrcode[pil])Pillow (auto-installed with qrcode)QRCodeGenerator/ │ ├── main.py # The QR code generator program ├── requirements.txt # Lists required libraries ├── inputs.txt # Optional file for batch input └── qrcodes/ # Folder where generated images are saved
main.py – The complete programrequirements.txt – qrcode[pil] (for pip install)inputs.txt – One URL/text per line for batch modeqrcodes/ – All output PNG files go heremkdir QRCodeGenerator
cd QRCodeGenerator
mkdir qrcodes
Create main.py and requirements.txt.
The qrcode library handles all the QR encoding and image generation. It depends on Pillow (PIL), which handles image manipulation and file saving.
pip install qrcode[pil]
[pil] tells pip to also install Pillow. Without it, qrcode can only generate text-art QR codes (using # and spaces), not PNG images.
Save the dependency:
# requirements.txt qrcode[pil]
A QR code is a grid of black and white squares called "modules." The structure is:
┌─────────────────────────────────┐ │ ┌─────┐ ┌─────┐ │ │ │ ███ │ FINDER │ ███ │ │ │ │ ███ │ PATTERNS │ ███ │ │ │ │ ███ │ │ ███ │ │ │ └─────┘ └─────┘ │ │ │ │ DATA MODULES │ │ (the actual content) │ │ │ │ ┌─────┐ │ │ │ ███ │ TIMING PATTERN │ │ └─────┘ │ │ │ │ ALIGNMENT PATTERN ──┐ │ │ │ │ └──────────────────────┴──────────┘ ──── BORDER (4 modules white) ────
Key components:
Start with the simplest possible generation — a QR code for a URL.
import qrcode
data = "https://www.python.org"
img = qrcode.make(data)
img.save("qrcodes/python_qr.png")
print("QR code generated and saved as qrcodes/python_qr.png")
qrcode.make(data) — This is the shortcut function. It uses default settings (version auto-detect, box_size=10, border=4, black on white) and returns a PIL Image object.
img.save("qrcodes/python_qr.png") — Saves the image to the qrcodes/ subfolder as a PNG file. The qrcodes/ folder must exist.
What you get: A 290×290 pixel PNG file (21 modules × 10px + 2×4 borders × 10px = 290px). Scan it with your phone camera to verify it opens python.org.
For more control, use the QRCode class instead of the make() shortcut.
import qrcode
from qrcode.constants import ERROR_CORRECT_M
qr = qrcode.QRCode(
version=1,
error_correction=ERROR_CORRECT_M,
box_size=10,
border=4
)
qr.add_data("https://www.python.org")
qr.make(fit=True)
img = qr.make_image()
img.save("qrcodes/custom_qr.png")
Parameters explained:
| Parameter | What it does | Typical value |
|---|---|---|
version | Controls the QR matrix size (1 = 21×21, 2 = 25×25, up to 40 = 177×177) | 1 for short text, auto if fit=True |
error_correction | How much damage the QR can survive and still scan | ERROR_CORRECT_M (15%) |
box_size | Size of each module in image pixels | 10 (produces a ~290px image) |
border | White border in modules (minimum 4 per spec) | 4 |
qr.add_data(data) — Encodes the data into the QR matrix. This is where the text is converted to the internal bit pattern.
qr.make(fit=True) — Automatically selects the smallest version that fits the data. With fit=False, it uses whatever version you set (which may be too small for long text → error).
Error correction allows QR codes to be scanned even when partially damaged or dirty.
from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_M, ERROR_CORRECT_Q, ERROR_CORRECT_H
| Level | Letter | Recovery | Best for |
|---|---|---|---|
| L | Low | ~7% | Large codes, clean printing |
| M | Medium | ~15% | General use (default) |
| Q | Quartile | ~25% | Industrial environments |
| H | High | ~30% | Small codes, damaged surfaces |
Trade-off: Higher error correction means more redundancy modules, which reduces the amount of data the QR code can hold. For a version 1 QR code:
For most purposes, ERROR_CORRECT_M is the right balance.
By default QR codes are black on white. You can change both colors.
img = qr.make_image(fill_color="darkblue", back_color="yellow")
img.save("qrcodes/colored_qr.png")
fill_color — The color of the dark modules (the actual QR pattern). Accepts any color name Pillow understands: "black", "darkblue", "green", "#FF0000", etc.
back_color — The background color.
Contrast rule (important for scanning):
DO use: DON'T use: black on white yellow on white (low contrast → won't scan) darkblue on white lightblue on white (low contrast) darkgreen on beige black on darkblue (too dark) #1a1a1a on white red on pink (similar brightness)
The scanner needs a strong difference between "dark" and "light" modules. Light foreground on dark background works too, but may scan slower on some phones.
Let the user specify the output filename. Sanitize it to remove characters that cause file system errors.
def save_qr(img, filename):
if not filename.endswith(".png"):
filename += ".png"
# Remove characters that are unsafe for filenames
safe_name = "".join(c for c in filename if c.isalnum() or c in "._- ")
path = f"qrcodes/{safe_name}"
img.save(path)
print(f"Saved as {path}")
endswith(".png") — Adds the extension automatically if the user forgot. Prevents accidentally saving a file without an extension.
Safe filename filter explained:
filename = "hello:world?query"
# For each character:
"h" → isalnum()? Yes → keep
"e" → isalnum()? Yes → keep
...
"?" → isalnum()? No. Is it in "._- "? No → remove
"q" → isalnum()? Yes → keep
...
Result: "helloworldquery"
Characters like /, \, :, *, ?, ", <, >, | are stripped because they are invalid in filenames on Windows or have special meanings.
Add a prompt that asks what the QR code should contain.
print("=== QR Code Generator ===\n")
data = input("Enter text or URL for the QR code: ")
if not data.strip():
print("You must enter some text or a URL.")
else:
qr = qrcode.QRCode(version=1, error_correction=ERROR_CORRECT_M, box_size=10, border=4)
qr.add_data(data)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
filename = input("Enter filename (without .png): ")
save_qr(img, filename)
data.strip() — Removes leading/trailing whitespace. If the user typed nothing or just spaces, the string is empty and we show an error.
Better validation: reject empty input and warn about very long text.
def validate_data(data):
if not data.strip():
return False, "Input cannot be empty."
if len(data) > 2000:
print("Warning: Very long input. The QR code may be too dense to scan easily.")
print("Consider splitting the data into multiple QR codes.")
return True, None
Why warn at 2000 characters: A version 40 QR code with L error correction can hold up to ~4296 alphanumeric characters. Beyond 2000, the code becomes very dense with tiny modules, making it harder for phone cameras to scan reliably.
Using validate_data: The function returns a tuple (valid, error_msg). If the input passes, valid is True and error_msg is None. If it fails, valid is False and error_msg describes the problem. You can use it like this:
data = input("Enter text or URL: ")
valid, error_msg = validate_data(data)
if not valid:
print(error_msg)
# exit or ask again
This pattern will be used in the final version of the program.
Read multiple inputs from a text file — one per line — and generate a QR code for each.
Create inputs.txt:
https://www.google.com
https://www.github.com
Hello World!
https://stackoverflow.com
Batch generator function:
def batch_generate(filepath):
try:
with open(filepath, "r") as f:
lines = [line.strip() for line in f if line.strip()]
except FileNotFoundError:
print(f"File {filepath} not found.")
return
print(f"Generating {len(lines)} QR codes from {filepath}...\n")
for i, line in enumerate(lines, 1):
qr = qrcode.QRCode(version=1, error_correction=ERROR_CORRECT_M, box_size=10, border=4)
qr.add_data(line)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
# Create a safe short filename from the first 20 chars
short_name = "".join(c for c in line[:20] if c.isalnum() or c in " _-")
filename = f"qrcodes/batch_{i}_{short_name}.png"
img.save(filename)
print(f" [{i}/{len(lines)}] {filename}")
print("\nBatch complete!")
with open(filepath, "r") as f: — Opens the file for reading. The with block automatically closes the file when done.
[line.strip() for line in f if line.strip()] — A list comprehension. For each line in the file, strip whitespace and skip empty lines. This removes the \n at the end of each line and ignores blank lines.
line[:20] — Truncates to 20 characters for filenames. Prevents absurdly long filenames if someone puts a 500-character URL in the file.
Here are the core functions before assembling the main menu:
import qrcode
from qrcode.constants import ERROR_CORRECT_M
def save_qr(img, filename):
if not filename.endswith(".png"):
filename += ".png"
safe_name = "".join(c for c in filename if c.isalnum() or c in "._- ")
path = f"qrcodes/{safe_name}"
img.save(path)
print(f"Saved as {path}")
def batch_generate(filepath):
try:
with open(filepath, "r") as f:
lines = [line.strip() for line in f if line.strip()]
except FileNotFoundError:
print(f"File {filepath} not found.")
return
print(f"Generating {len(lines)} QR codes...\n")
for i, line in enumerate(lines, 1):
qr = qrcode.QRCode(version=1, error_correction=ERROR_CORRECT_M, box_size=10, border=4)
qr.add_data(line)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
short_name = "".join(c for c in line[:20] if c.isalnum() or c in " _-")
filename = f"qrcodes/batch_{i}_{short_name}.png"
img.save(filename)
print(f" [{i}/{len(lines)}] {filename}")
print("\nBatch complete!")
import qrcode
from qrcode.constants import ERROR_CORRECT_M
def save_qr(img, filename):
"""Save QR code image with a safe filename."""
if not filename.endswith(".png"):
filename += ".png"
safe_name = "".join(c for c in filename if c.isalnum() or c in "._- ")
path = f"qrcodes/{safe_name}"
img.save(path)
print(f"Saved as {path}")
def batch_generate(filepath):
"""Read file line by line, generate a QR code for each line."""
try:
with open(filepath, "r") as f:
lines = [line.strip() for line in f if line.strip()]
except FileNotFoundError:
print(f"File {filepath} not found.")
return
print(f"Generating {len(lines)} QR codes from {filepath}...\n")
for i, line in enumerate(lines, 1):
qr = qrcode.QRCode(version=1, error_correction=ERROR_CORRECT_M, box_size=10, border=4)
qr.add_data(line)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
short_name = "".join(c for c in line[:20] if c.isalnum() or c in " _-")
filename = f"qrcodes/batch_{i}_{short_name}.png"
img.save(filename)
print(f" [{i}/{len(lines)}] {filename}")
print("\nBatch complete!")
def validate_data(data):
"""Check that input is not empty. Warn if very long."""
if not data.strip():
return False, "Input cannot be empty."
if len(data) > 2000:
print("Warning: Very long input. The QR code may be too dense to scan easily.")
return True, None
def main():
print("=== QR Code Generator ===\n")
print("1. Generate a single QR code")
print("2. Batch generate from file")
choice = input("\nChoose (1 or 2): ")
if choice == "1":
data = input("Enter text or URL: ")
valid, error_msg = validate_data(data)
if not valid:
print(error_msg)
return
qr = qrcode.QRCode(version=1, error_correction=ERROR_CORRECT_M, box_size=10, border=4)
qr.add_data(data)
qr.make(fit=True)
use_color = input("Use custom colors? (yes/no): ").lower()
if use_color == "yes":
fg = input("Foreground color (e.g., darkblue): ")
bg = input("Background color (e.g., yellow): ")
img = qr.make_image(fill_color=fg, back_color=bg)
else:
img = qr.make_image(fill_color="black", back_color="white")
filename = input("Save as (without .png): ")
save_qr(img, filename)
elif choice == "2":
filepath = input("Enter path to input file (e.g., inputs.txt): ")
batch_generate(filepath)
else:
print("Invalid choice.")
if __name__ == "__main__":
main()
Screenshot Suggestion: File explorer showing
qrcodes/folder with several PNG files, and one QR code image opened in the default image viewer.
Run the file:
python main.py
Test 1 – Generate a QR code for a URL
Input: "https://www.python.org"
Expected: QR code saved to qrcodes/. Scan with phone camera → opens python.org.
Test 2 – Generate a QR code for plain text
Input: "Hello World"
Expected: QR code saved. When scanned, phone displays "Hello World" (not a URL).
Test 3 – Custom colors
Choose custom colors, input Foreground: "darkgreen", Background: "white"
Expected: QR code with green modules on white background. Should still scan.
Test 4 – Empty input
Input: (press Enter with no text)
Expected: "Input cannot be empty." Program returns to menu or exits.
Test 5 – Batch generation
Select batch mode (2). Create inputs.txt with 3 lines.
Expected: 3 PNG files created in qrcodes/ named batch_1_..., batch_2_..., batch_3_...
Test 6 – FileNotFoundError in batch mode
Enter a non-existent file path like "nonexistent.txt".
Expected: "File nonexistent.txt not found." Program returns.
ModuleNotFoundError: No module named 'qrcode'
Trigger: Running the program without installing the qrcode library.
Why: qrcode is a third-party package, not part of Python's standard library.
Fix: Run `pip install qrcode[pil]`.
OSError: cannot write mode P as PNG
Trigger: Using certain custom colors with the PAL mode image.
Why: Some color combinations produce an indexed (P) mode image that cannot be saved as PNG directly.
Fix: Add `.convert("RGB")`: `img = qr.make_image(fill_color=fg, back_color=bg).convert("RGB")`
FileNotFoundError when batch generating
Trigger: The path to the input file is wrong or the file does not exist.
Why: `open(filepath, "r")` raises FileNotFoundError when the path is invalid.
Fix: Check that the file exists and the path is correct. Use an absolute path if needed.
QR code does not scan
Trigger: Low contrast colors (yellow on white, light blue on white).
Why: The scanner cannot distinguish between modules and background.
Fix: Use dark foreground (black, darkblue, #1a1a1a) on light background (white, #f5f5f5).
ValueError: Data too long for version
Trigger: The data is longer than the specified version can hold.
Why: Version 1 holds ~16 alphanumeric chars with M error correction.
Fix: Use `qr.make(fit=True)` to auto-select the smallest adequate version.
paste()https:// if missingWi-Fi QR Code – Generate a QR code that connects to a Wi-Fi network using the format: WIFI:T:WPA;S:YourNetworkName;P:YourPassword;;. Scanning it prompts the phone to join the network.
vCard QR Code – Generate a contact QR code using vCard format: BEGIN:VCARD\nVERSION:3.0\nFN:Name\nTEL:number\nEMAIL:email\nEND:VCARD. Scanning it saves a contact to the phone's address book.
Bulk CSV Import – Read from a CSV file where each row has a name and a url. Generate QR codes named after each name (e.g., google.png for https://google.com).
Error Correction Toggle – Add a menu option that lets the user choose error correction level (L, M, Q, H). Show how many characters each level can hold for the data they entered.
QR Code Decoder – Install pyzbar and write a decode function that reads a QR code image using pyzbar.decode() and prints its content back to the terminal. This completes the encode → decode round-trip.
Save your progress and earn XP for completing tutorials.
Keep learning
Technology
Python
Lesson group
Mini Projects
Progress
91% complete