You can build a working local URL shortener and render QR codes in a terminal using only Python’s standard library. The example below stores short links in SQLite, creates hard-to-guess codes with secrets, and implements a deliberately small QR encoder rather than calling a QR package. Its QR encoder supports byte-mode payloads up to 32 UTF-8 bytes; that is enough for the example’s local short URLs, but it is not a general-purpose QR implementation.
What this implementation does—and what it does not
The shortener runs on your machine at http://localhost:8000. Add a destination, start the HTTP server, and visit the resulting /s/<code> address to redirect to it. The mapping persists in a local SQLite database.
As an Amazon Associate I earn from qualifying purchases.
The QR encoder handles Version 2, error-correction level L, byte mode, and a fixed mask pattern. It builds the matrix, including Reed–Solomon error-correction codewords and format information, then prints a terminal-friendly rendering. The QR standard covers more modes, versions, correction levels, and production requirements than this implementation supports. Treat this as a constrained educational encoder, not a standards-complete substitute for a maintained QR library.
- Requires Python 3.10 or later and no third-party packages.
- Accepts only HTTP and HTTPS destinations for new short links.
- Accepts QR payloads of at most 32 UTF-8 bytes. Longer input is rejected rather than silently truncated.
- Uses a local-only listening address by default; it is not a public service configuration.
Save the complete program
Save this as shortqr.py. The imports are all from Python’s standard library; in particular, no QR package is used to generate the matrix.
#1 Best Overall
import argparse
import secrets
import sqlite3
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlsplit
DB = "shortlinks.sqlite3"
BASE_URL = "http://localhost:8000"
def connect():
db = sqlite3.connect(DB)
db.execute("""CREATE TABLE IF NOT EXISTS links (
code TEXT PRIMARY KEY,
destination TEXT NOT NULL
)""")
return db
def validate_destination(value):
if not value or any(ch.isspace() or ord(ch) < 32 or ord(ch) == 127
for ch in value):
raise ValueError("URL must be non-empty and contain no whitespace or control characters")
try:
parts = urlsplit(value)
port = parts.port # Accessing this also detects malformed port values.
except ValueError as exc:
raise ValueError("Malformed URL authority or port") from exc
if parts.scheme.lower() not in ("http", "https"):
raise ValueError("Only http and https destinations are accepted")
if not parts.hostname:
raise ValueError("URL must include a hostname")
if parts.username is not None or parts.password is not None:
raise ValueError("URLs containing embedded credentials are not accepted")
return value
def add_link(destination):
destination = validate_destination(destination)
db = connect()
try:
for _ in range(8):
code = secrets.token_urlsafe(5)
try:
with db:
db.execute("INSERT INTO links(code, destination) VALUES (?, ?)",
(code, destination))
return code
except sqlite3.IntegrityError:
continue
raise RuntimeError("Could not allocate a unique short code; try again")
finally:
db.close()
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
parts = urlsplit(self.path)
prefix = "/s/"
if not parts.path.startswith(prefix):
self.send_error(404, "Short link not found")
return
code = parts.path[len(prefix):]
if not code or "/" in code:
self.send_error(404, "Short link not found")
return
db = connect()
try:
row = db.execute("SELECT destination FROM links WHERE code = ?",
(code,)).fetchone()
finally:
db.close()
if row is None:
self.send_error(404, "Short link not found")
return
try:
destination = validate_destination(row[0])
except ValueError:
self.send_error(500, "Stored destination is invalid")
return
self.send_response(302)
self.send_header("Location", destination)
self.end_headers()
def gf_mul(x, y):
"""Multiply bytes in GF(256), using the QR field polynomial 0x11D."""
z = 0
for _ in range(8):
if y & 1:
z ^= x
y >>= 1
x <<= 1
if x & 0x100:
x ^= 0x11D
return z
def rs_remainder(data, degree=10):
generator = [1]
root = 1
for _ in range(degree):
nxt = [0] * (len(generator) + 1)
for i, coefficient in enumerate(generator):
nxt[i] ^= coefficient
nxt[i + 1] ^= gf_mul(coefficient, root)
generator = nxt
root = gf_mul(root, 2)
remainder = [0] * degree
for byte in data:
factor = byte ^ remainder[0]
remainder = remainder[1:] + [0]
for i, coefficient in enumerate(generator[1:]):
remainder[i] ^= gf_mul(coefficient, factor)
return remainder
def append_bits(bits, value, count):
for shift in range(count - 1, -1, -1):
bits.append((value >> shift) & 1)
def qr_matrix(payload):
"""Encode up to 32 UTF-8 bytes as a Version 2-L QR matrix."""
raw = payload.encode("utf-8")
if len(raw) > 32:
raise ValueError("Version 2-L byte mode supports at most 32 UTF-8 bytes")
# Version 2-L has 34 data codewords and 10 error-correction codewords.
bits = []
append_bits(bits, 0b0100, 4) # Byte-mode indicator
append_bits(bits, len(raw), 8) # Character count for Versions 1–9
for byte in raw:
append_bits(bits, byte, 8)
capacity = 34 * 8
bits.extend([0] * min(4, capacity - len(bits)))
while len(bits) % 8:
bits.append(0)
data = []
for i in range(0, len(bits), 8):
value = 0
for bit in bits[i:i + 8]:
value = (value << 1) | bit
data.append(value)
pad = (0xEC, 0x11)
while len(data) < 34:
data.append(pad[(len(data) - ((len(bits) - 4 - 8 - 8 * len(raw) + 7) // 8)) % 2])
codewords = data + rs_remainder(data)
stream = []
for byte in codewords:
append_bits(stream, byte, 8)
size = 25
grid = [[None] * size for _ in range(size)]
def set_function(x, y, dark):
grid[y][x] = bool(dark)
def finder(cx, cy):
for dy in range(-4, 5):
for dx in range(-4, 5):
x, y = cx + dx, cy + dy
if 0 <= x < size and 0 <= y < size:
distance = max(abs(dx), abs(dy))
set_function(x, y, distance != 2 and distance != 4)
finder(3, 3)
finder(size - 4, 3)
finder(3, size - 4)
# Timing patterns.
for i in range(8, size - 8):
set_function(i, 6, i % 2 == 0)
set_function(6, i, i % 2 == 0)
# Version 2 alignment pattern, centered at (18, 18).
for dy in range(-2, 3):
for dx in range(-2, 3):
set_function(18 + dx, 18 + dy,
max(abs(dx), abs(dy)) != 1)
# Reserve the format-information modules before placing data.
for i in range(6):
set_function(8, i, False)
set_function(8, 7, False)
set_function(8, 8, False)
set_function(7, 8, False)
for i in range(9, 15):
set_function(14 - i, 8, False)
for i in range(8):
set_function(size - 1 - i, 8, False)
for i in range(8, 15):
set_function(8, size - 15 + i, False)
set_function(8, size - 8, True) # Fixed dark module
# Place data in the standard two-column zigzag, skipping function modules.
bit_index = 0
upward = True
right = size - 1
while right >= 1:
if right == 6:
right = 5
for vert in range(size):
y = size - 1 - vert if upward else vert
for x in (right, right - 1):
if grid[y][x] is None:
bit = stream[bit_index] if bit_index < len(stream) else 0
bit_index += 1
# Fixed mask pattern 0: (row + column) is even.
grid[y][x] = bool(bit ^ int((x + y) % 2 == 0))
upward = not upward
right -= 2
# Format: error correction L (01), mask pattern 0 (000).
format_data = 0b01000
remainder = format_data
for _ in range(10):
remainder = (remainder << 1) ^ (0x537 if remainder & 0x200 else 0)
format_bits = ((format_data << 10) | remainder) ^ 0x5412
def format_bit(i):
return bool((format_bits >> i) & 1)
for i in range(6):
set_function(8, i, format_bit(i))
set_function(8, 7, format_bit(6))
set_function(8, 8, format_bit(7))
set_function(7, 8, format_bit(8))
for i in range(9, 15):
set_function(14 - i, 8, format_bit(i))
for i in range(8):
set_function(size - 1 - i, 8, format_bit(i))
for i in range(8, 15):
set_function(8, size - 15 + i, format_bit(i))
return grid
def print_qr(payload):
matrix = qr_matrix(payload)
border = " " * 8
print(border)
for row in matrix:
print(border + "".join("██" if cell else " " for cell in row) + border)
print(border)
def main():
parser = argparse.ArgumentParser(description="Local URL shortener and small ASCII QR encoder")
sub = parser.add_subparsers(dest="command", required=True)
add = sub.add_parser("add", help="store a destination and print its short URL")
add.add_argument("url")
serve = sub.add_parser("serve", help="run the local redirect server")
serve.add_argument("--host", default="127.0.0.1")
serve.add_argument("--port", type=int, default=8000)
qr = sub.add_parser("qr", help="render a payload of at most 32 UTF-8 bytes")
qr.add_argument("payload")
args = parser.parse_args()
if args.command == "add":
code = add_link(args.url)
print(f"{BASE_URL}/s/{code}")
elif args.command == "serve":
server = HTTPServer((args.host, args.port), Handler)
print(f"Serving on http://{args.host}:{args.port}")
try:
server.serve_forever()
except KeyboardInterrupt:
pass
finally:
server.server_close()
else:
try:
print_qr(args.payload)
except ValueError as exc:
print(exc, file=sys.stderr)
raise SystemExit(2)
if __name__ == "__main__":
main()
Run the shortener locally
Add a destination
In a terminal, create a link. Use a real HTTP or HTTPS destination in place of the example:
python shortqr.py add https://example.org/page
The program prints a short URL such as http://localhost:8000/s/AbCdEfG. The code is generated with secrets.token_urlsafe(5); the database enforces uniqueness, and the program retries a collision up to eight times. SQLite stores the destination in shortlinks.sqlite3 in the current directory.
Rank #2
Start the redirect server
python shortqr.py serve
Open the printed short URL while the server is running. Existing codes redirect with HTTP 302; unknown or malformed codes receive a 404 response. Stop the server with Ctrl+C. The default bind address, 127.0.0.1, makes it reachable only from the same machine.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Generate and display a QR code
Pass the printed short URL directly to the QR subcommand. The example short URL is designed to fit the encoder’s 32-byte limit:
python shortqr.py qr "http://localhost:8000/s/AbCdEfG"
Use a terminal font with fixed-width glyphs. Each module is rendered as two characters wide to reduce distortion from glyph proportions, with a four-module quiet zone around the symbol. The encoder uses a single fixed mask instead of scoring all masks for visual balance, so appearance is not optimized. Scan the rendered result with the device and environment you intend to use; appearance alone does not establish scanner compatibility.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.How the QR encoder fits together
Data and error correction
The encoder converts UTF-8 bytes into QR byte mode, adds the byte count, terminator and padding, then creates ten Reed–Solomon error-correction codewords over GF(256). Version 2-L has 34 data codewords and 10 error-correction codewords in this implementation. The mode and count fields are encoded explicitly; payloads beyond 32 bytes are rejected.
Matrix construction
The 25-by-25 grid reserves the three finder patterns, separators, timing patterns, the Version 2 alignment pattern, format information and fixed dark module. Data bits are then placed in the QR zigzag pattern, masked with pattern 0, and paired with format bits for correction level L and that mask. Seven remainder modules are handled as zero data bits before masking.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchThis compact implementation intentionally supports one QR version and one correction level. The third-party qrcode project documents L, M, Q and H correction levels with approximate capacities of up to 7%, 15%, 25% and 30% respectively; those are package documentation figures, not measurements of this program. A general-purpose QR encoder needs broader version selection and additional standard-defined handling. ISO/IEC 18004:2024, published in August 2024, specifies QR symbology including encoding, dimensions, error correction and production quality.
Best Value
URL validation and public-service risks
Python’s urllib.parse.urlsplit() separates URL components; it does not establish that a URL is safe or valid. The program applies a narrow policy after parsing: an HTTP or HTTPS scheme, a hostname, no embedded username or password, and no whitespace or control characters. It also accesses parts.port so malformed port values are rejected.
That policy is a starting point for this local example, not a guarantee against phishing or every browser/parser discrepancy. OWASP recommends using a URL parser compatible with both the redirect API and browser interpretation when handling redirects. A public shortener also needs operational controls such as rate limits, abuse reporting and monitoring; unpredictable codes do not provide those protections. A QR code only encodes its payload, so users should still inspect destinations before opening them.
Useful changes and their trade-offs
Persisting links
SQLite keeps mappings after the process exits, unlike an in-memory dictionary, while avoiding an external database service. Back up the database if its mappings matter. The example does not implement deletion, administrative access, or concurrent deployment tuning.
Recommended Free Tools
Changing the QR payload limit
Do not simply raise the 32-byte check: the matrix version, data capacity, error-correction block structure and alignment patterns must all change consistently. For arbitrary URLs or production output, use a maintained QR library; that is a sensible alternative, but it would no longer be a from-scratch QR encoder.
Making the service public
Changing --host can expose the HTTP server beyond the local machine, but this tutorial is not a deployment guide. Before making a shortener publicly reachable, add abuse controls, safe operational logging, a review and reporting path, and a robust URL policy. Since a shortener conceals the destination behind a redirect, it can be used to disguise hostile links.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




