Skip to content

URL Shortener

A URL shortener compresses a long URL like https://example.com/very/long/path?query=1&foo=2 into something like https://tinyurl.com/abc123. The short link redirects to the original when clicked. In this tutorial you will start by using the pyshorteners library to talk to existing services (TinyURL, Bitly), then go deeper and build your own shortener with Flask, SQLite, and a base62 encoder — the same architecture every commercial shortener uses.

You will learn:

  • How third-party APIs wrap real services.
  • How HTTP redirects work.
  • How short codes are generated and looked up.
  • Why custom shorteners need careful URL validation.
  • How to add analytics (click counts, referrers) and rate limiting.
  • Python 3.6 or above.
  • A code editor or IDE.
  • An internet connection (for the third-party portion).
  • Comfort with pip install and basic web concepts.
install
pip install pyshorteners

For the “build your own” section later:

install
pip install flask
  1. Create a folder named url-shortener.
  2. Inside it, create urlshorter.py.
URL Shortener pch.viewSource
URL Shortener
# URL Shorter

import pyshorteners # pip install pyshorteners



url = input("Enter URL: ")
# TinyURL
shortener = pyshorteners.Shortener()
shortURL = shortener.tinyurl.short(url)

# Bitly
# bitlyShortener = pyshorteners.Shortener(api_key='01b6c587cskek4kdfijsjce4cf27ce2') # Change API Key with your own API Key
# shortURL = bitlyShortener.bitly.short(url) # bitly
print("Short URL: ", shortURL)
command
C:\Users\Your Name\url-shortener> python urlshorter.py
Enter URL: https://www.example.com/very/long/url/with/many/parameters?param1=value1&param2=value2
Short URL: https://tinyurl.com/abc123def
urlshorter.py
import pyshorteners
urlshorter.py
url = input("Enter URL: ").strip()
if not (url.startswith("http://") or url.startswith("https://")):
    raise SystemExit("URL must start with http:// or https://")
 
shortener = pyshorteners.Shortener()
short_url = shortener.tinyurl.short(url)
print("Short URL:", short_url)
  • TinyURL needs no API key.
  • Bitly does — set up an API token and use pyshorteners.Shortener(api_key="...").
urlshorter.py
from pyshorteners.exceptions import ShorteningErrorException, ExpandingErrorException
import requests
 
try:
    short_url = shortener.tinyurl.short(url)
except (ShorteningErrorException, requests.RequestException) as e:
    print("Could not shorten:", e)
ServiceNeeds API key?
TinyURLNo
Is.gdNo
V.gdNo
Clck.ruNo
Chilp.itNo
BitlyYes
QR.netNo
OwlyYes
CuttlyYes

Switching service is just changing the attribute: shortener.isgd.short(url), shortener.bitly.short(url), etc.

Using a service is fine. Building one yourself is far more educational and gives you full control.

  1. User submits a long URL.
  2. Server stores { short_code → long_url } in a database.
  3. Server returns https://short.example.com/<short_code>.
  4. When that short URL is visited, the server looks up the code and returns an HTTP 302 redirect to the long URL.

The short code is usually a base62 (0-9a-zA-Z) encoding of an auto-incrementing ID — 6 characters yields ~57 billion possible URLs.

own_shortener.py
from flask import Flask, request, redirect, jsonify
import sqlite3, string, secrets
 
app = Flask(__name__)
ALPHABET = string.ascii_letters + string.digits   # 62 characters
 
def init_db():
    with sqlite3.connect("urls.db") as db:
        db.execute("""CREATE TABLE IF NOT EXISTS urls (
            code TEXT PRIMARY KEY,
            url  TEXT NOT NULL,
            clicks INTEGER DEFAULT 0
        )""")
 
def new_code(length=6):
    return "".join(secrets.choice(ALPHABET) for _ in range(length))
 
@app.post("/shorten")
def shorten():
    long_url = request.json["url"].strip()
    if not long_url.startswith(("http://", "https://")):
        return jsonify(error="URL must include http:// or https://"), 400
    with sqlite3.connect("urls.db") as db:
        # generate codes until one is unique
        while True:
            code = new_code()
            try:
                db.execute("INSERT INTO urls(code, url) VALUES(?, ?)",
                           (code, long_url))
                break
            except sqlite3.IntegrityError:
                continue
    return jsonify(short=f"{request.host_url}{code}")
 
@app.get("/<code>")
def follow(code):
    with sqlite3.connect("urls.db") as db:
        row = db.execute("SELECT url FROM urls WHERE code=?", (code,)).fetchone()
        if not row:
            return "Not found", 404
        db.execute("UPDATE urls SET clicks = clicks + 1 WHERE code=?", (code,))
    return redirect(row[0], code=302)
 
if __name__ == "__main__":
    init_db()
    app.run(debug=True)

Test it:

test
curl -X POST http://localhost:5000/shorten -H "Content-Type: application/json" \
     -d '{"url":"https://python.org"}'
# {"short":"http://localhost:5000/aB3xY9"}
 
curl -I http://localhost:5000/aB3xY9
# HTTP/1.1 302 FOUND
# Location: https://python.org
  • 62 chars (09 a–z A–Z) is URL-safe and case-sensitive.
  • 6 characters = 62 ** 657 billion possible short URLs.
  • 8 characters = 218 trillion. Plenty.

Some shorteners use a counter (auto-increment ID) encoded as base62, which guarantees uniqueness without retries.

User input is a frequent vector for open-redirect attacks. Validate:

validate.py
from urllib.parse import urlparse
 
def is_valid_url(u: str) -> bool:
    try:
        p = urlparse(u)
        return p.scheme in {"http", "https"} and bool(p.netloc) and "." in p.netloc
    except Exception:
        return False

Reject javascript:, data:, file: schemes — they would let an attacker craft phishing-style short links.

ProblemCauseFix
pyshorteners.exceptions.ShorteningErrorExceptionProvider down, malformed URL, rate-limitedCatch and retry/back off
Same short code generated for two URLsDid not check uniquenessINSERT with PRIMARY KEY on code → catch IntegrityError
Redirect goes nowhereReturned a 200 with the URL in the bodyReturn redirect(url, code=302)
Open redirect to javascript:No URL validationWhitelist http/https schemes only
Database lock errorsSQLite with concurrent writersUse a real DB (Postgres) for multi-process deployments

Let the user request /mylink instead of a random code:

alias.py
alias = request.json.get("alias")
if alias and not db.execute("SELECT 1 FROM urls WHERE code=?", (alias,)).fetchone():
    code = alias

Add expires_at TIMESTAMP NULL. On follow, return 410 Gone if expired.

You already store clicks. Add a /stats/<code> endpoint that returns total clicks plus, optionally, referrer breakdown.

qr.py
import qrcode
img = qrcode.make(short_url)
img.save("link.png")

Add a password_hash column. Before redirecting, require the user to enter a password.

Accept a list of URLs and return short versions in one call.

Use flask-limiter to cap to e.g. 10 requests/minute per IP.

Allow shortening only for specific domains — useful for company-internal use.

Wrap the API client with argparse:

cli
python shorten.py https://example.com --alias homepage

Run behind gunicorn + nginx, point a domain like short.example.com at it, and you have your own production shortener for ~$5/month.

  • Short URLs are public. Anyone who guesses the code can follow the link. Do not embed sensitive paths.
  • Phishing risk. Short links hide their destination — bad actors abuse this. Consider a preview page (/p/<code>) that shows the destination before redirecting.
  • Log retention. Server access logs reveal who clicked what. Decide your retention policy.
  • Open-redirect is the classic shortener vulnerability — validate schemes carefully.
  • Social-media sharing (Twitter character limits used to drive this).
  • Email and SMS marketing — short, trackable URLs.
  • QR codes that point to a short URL you can rebind later.
  • Branded short links (e.g., nyti.ms for the New York Times).
  • Internal tooling — share links to dashboards without copying 200-character URLs.
  • Third-party APIs — calling, error handling, swap-ability.
  • HTTP redirects — status codes and what they mean.
  • Encoding schemes — base62 versus base64 versus hex.
  • Database integrity — unique constraints and how to handle collisions.
  • Web security basics — open redirect, input validation, abuse vectors.
  • Build the Flask version above; deploy it somewhere free (Render, Fly.io).
  • Add analytics with referrers, country (via MaxMind GeoIP), and time series.
  • Add a web frontend with a form and a copy-to-clipboard button.
  • Add authentication so each user only sees their own links.
  • Layer on rate limiting and captcha before opening to the public.

You used a third-party shortener and learned how the actual underlying mechanism works — a database lookup and an HTTP 302. Building your own version is one of the most concise demonstrations of how the modern web composes simple parts into useful services. Full source on GitHub. Explore more web projects on Python Central Hub.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading