iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more
You can build a working shortener with Python’s standard library: validate destinations, store unpredictable short codes in SQLite, and redirect requests through a small HTTP server. You can also encode a deliberately limited QR Code subset yourself—without a QR package—and render its modules as terminal text. The implementation below supports QR Code version 1-L in byte mode, so it accepts at most 17 UTF-8 bytes per payload. It is a learning implementation, not a general-purpose QR encoder or a production-ready public shortener.
What this implementation does—and where it stops
- Uses
urllib.parse,sqlite3,secrets, and other Python standard-library modules. No third-party QR library generates the symbol. - Accepts only absolute HTTP or HTTPS destinations with a hostname and no embedded username or password.
- Stores a mapping between an eight-character URL-safe token and its destination in SQLite, then redirects known tokens.
- Creates QR Code version 1-L symbols in byte mode and prints them using doubled ASCII module widths and a four-module quiet zone.
- Does not implement other QR versions, error-correction levels, QR input modes, or mask selection. The generated matrix has not been scanner-tested here; verify it with an independent decoder before relying on it.
Version 1-L has 19 data codewords. Byte mode uses four mode bits and an eight-bit length field, leaving room for at most 17 payload bytes. The capacity is a poor fit for many full destination URLs, which is why the example encodes the short URL instead. Even a short URL may exceed the limit; the program reports that rather than truncating it.
Build the shortener
Validate destinations before storing them
urllib.parse.urlsplit() separates URL components; it does not certify that an input is safe. Python’s urllib.parse documentation explicitly warns that parsing APIs do not validate inputs. OWASP’s Unvalidated Redirects and Forwards guidance likewise stresses using a URL parser compatible with both the redirect API and browser interpretation. A scheme and hostname check is a reasonable starting policy for this local demonstration, not a complete defense for a public service.
This policy rejects whitespace and control characters, missing hostnames, unexpected schemes, embedded credentials, and invalid ports. It deliberately does not rewrite the URL: transformations can change how a browser interprets a destination. A public product needs a stronger, documented policy and a way to respond to abuse.
#1 Best Overall
Save mappings and generate codes
Python’s secrets module is intended for security-sensitive random values; random is not. The script uses URL-safe token generation and a unique database constraint, then retries a bounded number of times if a candidate collides. Unpredictable codes do not replace rate limits, access controls for private links, or abuse monitoring.
Save this as shortener.py and run it with Python 3. The public base URL is fixed to the local address for this example; change it to the externally reachable origin when deploying, and ensure it matches the URL users should receive.
Rank #2
import json
import os
import re
import secrets
import sqlite3
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlsplit
DB_PATH = "shortlinks.sqlite3"
BASE_URL = os.environ.get("SHORTENER_BASE_URL", "http://127.0.0.1:8000").rstrip("/")
CODE_RE = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
def connect():
return sqlite3.connect(DB_PATH)
def initialize():
with connect() as db:
db.execute("""CREATE TABLE IF NOT EXISTS links (
code TEXT PRIMARY KEY,
destination TEXT NOT NULL
)""")
def validate_destination(value):
if not isinstance(value, str) or not value:
raise ValueError("destination must be a non-empty URL string")
if any(ord(ch) < 0x20 or ch.isspace() for ch in value):
raise ValueError("destination cannot contain whitespace or control characters")
try:
parts = urlsplit(value)
# Accessing .port also detects malformed or out-of-range port numbers.
_ = parts.port
except ValueError as exc:
raise ValueError("destination has a malformed authority or port") from exc
if parts.scheme.lower() not in ("http", "https"):
raise ValueError("only http and https destinations are allowed")
if not parts.netloc or not parts.hostname:
raise ValueError("destination must include a hostname")
if parts.username is not None or parts.password is not None:
raise ValueError("embedded credentials are not allowed")
return value
def create_short_link(destination):
destination = validate_destination(destination)
for _ in range(8):
code = secrets.token_urlsafe(6)
try:
with connect() as db:
db.execute("INSERT INTO links(code, destination) VALUES (?, ?)",
(code, destination))
return BASE_URL + "/" + code
except sqlite3.IntegrityError:
# A code collision is rare, but uniqueness is enforced by SQLite.
continue
raise RuntimeError("could not allocate a unique code after 8 attempts")
class ShortenerHandler(BaseHTTPRequestHandler):
def reply(self, status, payload):
body = json.dumps(payload).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_POST(self):
if self.path != "/api/shorten":
self.reply(404, {"error": "not found"})
return
try:
length = int(self.headers.get("Content-Length", "0"))
except ValueError:
self.reply(400, {"error": "invalid Content-Length"})
return
if length <= 0 or length > 8192:
self.reply(413, {"error": "request body must be 1–8192 bytes"})
return
try:
data = json.loads(self.rfile.read(length))
short_url = create_short_link(data.get("url"))
except (UnicodeDecodeError, json.JSONDecodeError, AttributeError):
self.reply(400, {"error": "send a JSON object containing a url string"})
except ValueError as exc:
self.reply(400, {"error": str(exc)})
except RuntimeError as exc:
self.reply(503, {"error": str(exc)})
else:
self.reply(201, {"short_url": short_url})
def do_GET(self):
code = self.path[1:] if self.path.startswith("/") else ""
if not CODE_RE.fullmatch(code):
self.reply(404, {"error": "short link not found"})
return
with connect() as db:
row = db.execute("SELECT destination FROM links WHERE code = ?",
(code,)).fetchone()
if row is None:
self.reply(404, {"error": "short link not found"})
return
# Recheck in case the database was edited outside this application.
try:
destination = validate_destination(row[0])
except ValueError:
self.reply(410, {"error": "stored destination is no longer permitted"})
return
self.send_response(302)
self.send_header("Location", destination)
self.send_header("Content-Length", "0")
self.end_headers()
if __name__ == "__main__":
initialize()
print("Listening on http://127.0.0.1:8000")
HTTPServer(("127.0.0.1", 8000), ShortenerHandler).serve_forever()
Try the HTTP endpoint
Start the server with python shortener.py. In another terminal, send a JSON request:
Free tools Windows power users keep installed
One-click scans. No signup required.
curl -i -X POST http://127.0.0.1:8000/api/shorten
-H 'Content-Type: application/json'
-d '{"url":"https://example.org/path"}'
A successful response is HTTP 201 with a JSON short_url. Open that URL to exercise the redirect. An unknown code returns 404. The example binds to loopback, so other machines cannot reach it; the request handler is also single-threaded and has no rate limiting, authentication, TLS termination, abuse reporting, or operational monitoring. Do not expose it as a public service unchanged.
Encode a limited QR Code without a package
What the encoder has to do
A QR symbol is not just text arranged in a square. ISO/IEC 18004:2024, the fourth edition published in August 2024, covers encoding, symbol formats and dimensions, error correction, decoding, and production quality. This small encoder implements one narrow path through those rules: version 1-L, byte mode, one Reed–Solomon block, and mask pattern 0. It rejects longer payloads rather than silently creating a malformed or truncated symbol.
Save the following as qr_ascii.py. The code constructs the data bitstream, adds error-correction bytes in the QR finite field, draws fixed patterns, places masked data bits, writes format information, and renders a quiet zone. The rendering doubles each module horizontally to help compensate for terminal glyph proportions; use a monospaced terminal.
SIZE = 21 # QR Code version 1 is 21 x 21 modules.
def gf_tables():
exp = [0] * 512
log = [0] * 256
value = 1
for i in range(255):
exp[i] = value
log[value] = i
value <<= 1
if value & 0x100:
value ^= 0x11D
for i in range(255, 512):
exp[i] = exp[i - 255]
return exp, log
GF_EXP, GF_LOG = gf_tables()
def gf_mul(a, b):
if a == 0 or b == 0:
return 0
return GF_EXP[GF_LOG[a] + GF_LOG[b]]
def poly_multiply(a, b):
result = [0] * (len(a) + len(b) - 1)
for i, x in enumerate(a):
for j, y in enumerate(b):
result[i + j] ^= gf_mul(x, y)
return result
def reed_solomon_remainder(data, degree=7):
generator = [1]
for i in range(degree):
generator = poly_multiply(generator, [1, GF_EXP[i]])
remainder = [0] * degree
for byte in data:
factor = byte ^ remainder[0]
remainder = remainder[1:] + [0]
for i in range(degree):
remainder[i] ^= gf_mul(generator[i + 1], factor)
return remainder
def append_bits(target, value, count):
for shift in range(count - 1, -1, -1):
target.append((value >> shift) & 1)
def make_codewords(text):
payload = text.encode("utf-8")
if len(payload) > 17:
raise ValueError("version 1-L byte mode supports at most 17 UTF-8 bytes")
bits = []
append_bits(bits, 0b0100, 4) # byte mode
append_bits(bits, len(payload), 8)
for byte in payload:
append_bits(bits, byte, 8)
capacity = 19 * 8
bits.extend([0] * min(4, capacity - len(bits))) # terminator
while len(bits) % 8:
bits.append(0)
data = []
for offset in range(0, len(bits), 8):
byte = 0
for bit in bits[offset:offset + 8]:
byte = (byte << 1) | bit
data.append(byte)
pad = (0xEC, 0x11)
while len(data) < 19:
data.append(pad[(len(data) - ((len(bits) + 7) // 8)) % 2])
return data + reed_solomon_remainder(data)
def set_finder(matrix, top, left):
for dy in range(-1, 8):
for dx in range(-1, 8):
row, col = top + dy, left + dx
if 0 <= row < SIZE and 0 <= col < SIZE:
if 0 <= dy < 7 and 0 <= dx < 7:
dark = (dy in (0, 6) or dx in (0, 6) or
(2 <= dy <= 4 and 2 <= dx <= 4))
matrix[row][col] = dark
else:
matrix[row][col] = False # finder separator
def format_bits(mask=0):
# Error-correction level L has format indicator 01.
value = (0b01 << 3) | mask
remainder = value << 10
for bit in range(14, 9, -1):
if (remainder >> bit) & 1:
remainder ^= 0x537 << (bit - 10)
return ((value << 10) | remainder) ^ 0x5412
def draw_format(matrix, mask=0):
bits = format_bits(mask)
first = ([(i, 8) for i in range(6)] + [(7, 8), (8, 8), (8, 7)] +
[(8, 14 - i) for i in range(9, 15)])
second = ([(8, SIZE - 1 - i) for i in range(8)] +
[(SIZE - 15 + i, 8) for i in range(8, 15)])
for i, (row, col) in enumerate(first):
matrix[row][col] = bool((bits >> i) & 1)
for i, (row, col) in enumerate(second):
matrix[row][col] = bool((bits >> i) & 1)
matrix[SIZE - 8][8] = True # fixed dark module
def make_matrix(text):
codewords = make_codewords(text)
stream = []
for byte in codewords:
append_bits(stream, byte, 8)
matrix = [[None] * SIZE for _ in range(SIZE)]
set_finder(matrix, 0, 0)
set_finder(matrix, 0, SIZE - 7)
set_finder(matrix, SIZE - 7, 0)
for i in range(8, SIZE - 8):
timing_dark = (i % 2 == 0)
if matrix[6][i] is None:
matrix[6][i] = timing_dark
if matrix[i][6] is None:
matrix[i][6] = timing_dark
# Reserve the format-information cells before placing payload bits.
format_positions = ([(i, 8) for i in range(6)] + [(7, 8), (8, 8), (8, 7)] +
[(8, 14 - i) for i in range(9, 15)] +
[(8, SIZE - 1 - i) for i in range(8)] +
[(SIZE - 15 + i, 8) for i in range(8, 15)])
for row, col in format_positions:
matrix[row][col] = False
matrix[SIZE - 8][8] = True
bit_index = 0
upward = True
right = SIZE - 1
while right > 0:
if right == 6:
right -= 1 # timing column is never a data column
rows = range(SIZE - 1, -1, -1) if upward else range(SIZE)
for row in rows:
for col in (right, right - 1):
if matrix[row][col] is None:
bit = stream[bit_index] if bit_index < len(stream) else 0
bit_index += 1
# Mask pattern 0: invert when row + column is even.
matrix[row][col] = bool(bit ^ ((row + col) % 2 == 0))
upward = not upward
right -= 2
draw_format(matrix, mask=0)
return matrix
def render_ascii(matrix):
border = 4
width = len(matrix) + border * 2
lines = []
for row in [[False] * border + line + [False] * border for line in matrix]:
lines.append("".join("██" if cell else " " for cell in row))
blank = " " * width
return "n".join([blank] * border + lines + [blank] * border)
if __name__ == "__main__":
import sys
if len(sys.argv) != 2:
raise SystemExit("usage: python qr_ascii.py SHORT_URL")
print(render_ascii(make_matrix(sys.argv[1])))
Generate a short link, then render it
After creating a short link through the endpoint, pass that exact URL as one shell argument:
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitchespython qr_ascii.py 'http://127.0.0.1:8000/AbCdEf_g'
The local URL shown is 28 ASCII bytes, so it exceeds this encoder’s 17-byte limit and will correctly raise an error. A shorter deployed origin and compact code might fit, but measure the UTF-8 byte length rather than counting visible characters. To support realistic longer URLs, implement additional QR versions and their block layouts, alignment patterns, capacities, and other required rules, or use a maintained QR library instead of claiming this subset handles arbitrary links.
Best Value
For reference, the third-party qrcode project documents error-correction levels L, M, Q, and H with approximate capacities of up to 7%, 15%, 25%, and 30%, respectively. Those are package documentation values, not measurements of this implementation. Using that package to produce the QR matrix would no longer be a from-scratch encoder.
Security and reliability decisions before deployment
Redirect safety is an application policy
A URL shortener can conceal a phishing destination, and a QR code does not make its payload trustworthy. For a public service, define which destinations users may shorten, add rate controls and abuse reporting, and decide how to handle malicious or removed links. Parsing alone does not stop open-redirect abuse. If another process or administrator can change stored records, revalidate destinations before redirecting, as the sample does.
Storage and operational behavior
SQLite persists mappings across process restarts using a standard-library module. An in-memory dictionary is simpler for a throwaway demonstration but loses every mapping when the process stops. The sample’s database contains destinations in plaintext; protect the file and backups appropriately, and consider privacy and retention requirements for your use case. A public deployment also needs a production-capable server setup, TLS, observability, and operational safeguards; those are intentionally outside this local example.
Quick Recap
Test what matters
- Submit a valid HTTP and HTTPS URL, then confirm the stored code redirects to the original destination.
- Check that unsupported schemes, missing hostnames, credentials, malformed ports, whitespace, and control characters are rejected.
- Request an unknown code and confirm a 404; verify invalid request JSON and oversized bodies receive client errors.
- Check that QR input above the supported byte limit fails clearly instead of truncating.
- Decode generated symbols with an independent QR reader before asserting scanner compatibility or using them in a workflow.
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.

