Skip to content

URL Expander

Abstract

Shortened links (bit.lybit.ly, t.cot.co, tinyurltinyurl) hide their real destination — convenient for sharing, risky for clicking. A URL expander follows the chain of HTTP redirects and shows you where a short link actually lands before you visit it. In this tutorial you build a small Tkinter app around a single requestsrequests call, then turn it into a genuinely useful safety tool: it reveals the full redirect chain, flags suspicious hops, handles timeouts, expands links in bulk, and copies results to the clipboard.

You will leave understanding:

  • How HTTP redirects (301/302) work and how requestsrequests follows them.
  • The difference between a HEADHEAD and a GETGET request, and why HEADHEAD is cheaper here.
  • How to handle network errors without crashing the UI.
  • Why expanding a link is a security practice, not just a convenience.

Prerequisites

  • Python 3.6 or above.
  • A text editor or IDE.
  • The requestsrequests library: pip install requestspip install requests.
  • Tkinter (bundled with Python).
  • A basic grasp of HTTP requests and responses.

Getting Started

Create the project

  1. Create a folder named url-expanderurl-expander.
  2. Inside it, create url_expander.pyurl_expander.py.
  3. Install the dependency: pip install requestspip install requests.

Write the code

url_expander.pySource
url_expander.py
"""
URL Expander
 
A Python application that expands shortened URLs to their original form.
Features include:
- Accepting a shortened URL as input.
- Displaying the expanded URL.
"""
 
import requests
from tkinter import Tk, Label, Entry, Button, messagebox
 
 
def expand_url(short_url):
    """Expand a shortened URL to its original form."""
    try:
        response = requests.head(short_url, allow_redirects=True)
        return response.url
    except requests.RequestException as e:
        return str(e)
 
 
class URLExpanderApp:
    def __init__(self, root):
        self.root = root
        self.root.title("URL Expander")
 
        Label(root, text="Enter Shortened URL:").grid(row=0, column=0, padx=10, pady=10)
        self.url_entry = Entry(root, width=50)
        self.url_entry.grid(row=0, column=1, padx=10, pady=10)
 
        Button(root, text="Expand URL", command=self.expand_url).grid(row=1, column=0, columnspan=2, pady=10)
 
    def expand_url(self):
        """Handle the button click to expand the URL."""
        short_url = self.url_entry.get()
        if not short_url:
            messagebox.showerror("Error", "Please enter a URL.")
            return
 
        expanded_url = expand_url(short_url)
        messagebox.showinfo("Expanded URL", f"Original URL: {expanded_url}")
 
 
def main():
    root = Tk()
    app = URLExpanderApp(root)
    root.mainloop()
 
 
if __name__ == "__main__":
    main()
 
url_expander.py
"""
URL Expander
 
A Python application that expands shortened URLs to their original form.
Features include:
- Accepting a shortened URL as input.
- Displaying the expanded URL.
"""
 
import requests
from tkinter import Tk, Label, Entry, Button, messagebox
 
 
def expand_url(short_url):
    """Expand a shortened URL to its original form."""
    try:
        response = requests.head(short_url, allow_redirects=True)
        return response.url
    except requests.RequestException as e:
        return str(e)
 
 
class URLExpanderApp:
    def __init__(self, root):
        self.root = root
        self.root.title("URL Expander")
 
        Label(root, text="Enter Shortened URL:").grid(row=0, column=0, padx=10, pady=10)
        self.url_entry = Entry(root, width=50)
        self.url_entry.grid(row=0, column=1, padx=10, pady=10)
 
        Button(root, text="Expand URL", command=self.expand_url).grid(row=1, column=0, columnspan=2, pady=10)
 
    def expand_url(self):
        """Handle the button click to expand the URL."""
        short_url = self.url_entry.get()
        if not short_url:
            messagebox.showerror("Error", "Please enter a URL.")
            return
 
        expanded_url = expand_url(short_url)
        messagebox.showinfo("Expanded URL", f"Original URL: {expanded_url}")
 
 
def main():
    root = Tk()
    app = URLExpanderApp(root)
    root.mainloop()
 
 
if __name__ == "__main__":
    main()
 

Run it

command
C:\Users\Your Name\url-expander> python url_expander.py
# Paste a short URL (e.g. https://bit.ly/xyz) and click "Expand URL".
# A dialog shows the final destination.
command
C:\Users\Your Name\url-expander> python url_expander.py
# Paste a short URL (e.g. https://bit.ly/xyz) and click "Expand URL".
# A dialog shows the final destination.

Step-by-Step Explanation

1. The core: follow the redirects

url_expander.py
def expand_url(short_url):
    try:
        response = requests.head(short_url, allow_redirects=True)
        return response.url
    except requests.RequestException as e:
        return str(e)
url_expander.py
def expand_url(short_url):
    try:
        response = requests.head(short_url, allow_redirects=True)
        return response.url
    except requests.RequestException as e:
        return str(e)

Two ideas do all the work:

  • requests.headrequests.head asks only for the response headers, not the page body. Since we only care about the final URL, downloading the whole page would be wasteful.
  • allow_redirects=Trueallow_redirects=True tells requestsrequests to chase every LocationLocation header automatically. After the last hop, response.urlresponse.url holds the real destination.

Catching requests.RequestExceptionrequests.RequestException (the base class for all requestsrequests errors) means a dead link or DNS failure returns a message instead of crashing.

2. Input guard

url_expander.py
short_url = self.url_entry.get()
if not short_url:
    messagebox.showerror("Error", "Please enter a URL.")
    return
url_expander.py
short_url = self.url_entry.get()
if not short_url:
    messagebox.showerror("Error", "Please enter a URL.")
    return

Never trust an empty field. Bail early with a clear message.

3. Show the result

url_expander.py
expanded_url = expand_url(short_url)
messagebox.showinfo("Expanded URL", f"Original URL: {expanded_url}")
url_expander.py
expanded_url = expand_url(short_url)
messagebox.showinfo("Expanded URL", f"Original URL: {expanded_url}")

A popup is fine for one URL. For more, you’ll want a results panel (below).

Upgrade: Show the Full Redirect Chain

A single destination hides intermediate hops — and those hops are where shady redirectors live. response.historyresponse.history records every step:

chain.py
import requests
 
def redirect_chain(short_url, timeout=10):
    r = requests.get(short_url, allow_redirects=True, timeout=timeout)
    hops = [resp.url for resp in r.history] + [r.url]
    return hops
 
for i, url in enumerate(redirect_chain("https://bit.ly/example")):
    print(f"{i}: {url}")
chain.py
import requests
 
def redirect_chain(short_url, timeout=10):
    r = requests.get(short_url, allow_redirects=True, timeout=timeout)
    hops = [resp.url for resp in r.history] + [r.url]
    return hops
 
for i, url in enumerate(redirect_chain("https://bit.ly/example")):
    print(f"{i}: {url}")

Seeing bit.ly → tracking.example → final.combit.ly → tracking.example → final.com tells you far more than final.comfinal.com alone.

Add a Timeout (Always)

A request with no timeout can hang forever, freezing your app:

timeout.py
requests.head(short_url, allow_redirects=True, timeout=10)
timeout.py
requests.head(short_url, allow_redirects=True, timeout=10)

Rule of thumb: every network call in production code gets a timeouttimeout. Catch requests.Timeoutrequests.Timeout separately to tell the user “the server is slow” rather than “the URL is broken”.

A lightweight safety check before the user clicks:

safety.py
from urllib.parse import urlparse
 
SUSPICIOUS_TLDS = {".zip", ".mov", ".tk", ".xyz"}
 
def looks_risky(url):
    host = urlparse(url).netloc.lower()
    flags = []
    if any(url.lower().endswith(t) for t in SUSPICIOUS_TLDS):
        flags.append("unusual TLD")
    if url.count("//") > 1:
        flags.append("nested redirect")
    if "@" in url:
        flags.append("credentials in URL")
    return flags
safety.py
from urllib.parse import urlparse
 
SUSPICIOUS_TLDS = {".zip", ".mov", ".tk", ".xyz"}
 
def looks_risky(url):
    host = urlparse(url).netloc.lower()
    flags = []
    if any(url.lower().endswith(t) for t in SUSPICIOUS_TLDS):
        flags.append("unusual TLD")
    if url.count("//") > 1:
        flags.append("nested redirect")
    if "@" in url:
        flags.append("credentials in URL")
    return flags

This is heuristic, not a malware scanner — but “credentials in URL” and “nested redirect” catch a surprising number of phishing tricks.

Batch Mode

Expand a whole list at once and show results in a TextText widget instead of popups:

batch.py
def expand_many(self):
    urls = self.input_text.get("1.0", "end").splitlines()
    self.output_text.delete("1.0", "end")
    for url in filter(str.strip, urls):
        final = expand_url(url.strip())
        self.output_text.insert("end", f"{url.strip()}  ->  {final}\n")
batch.py
def expand_many(self):
    urls = self.input_text.get("1.0", "end").splitlines()
    self.output_text.delete("1.0", "end")
    for url in filter(str.strip, urls):
        final = expand_url(url.strip())
        self.output_text.insert("end", f"{url.strip()}  ->  {final}\n")

Now you can paste 50 links and audit them in one click.

Common Mistakes

ProblemCauseFix
App hangs on a bad linkNo timeouttimeout setPass timeout=10timeout=10 to every request
Some shorteners don’t expandThey block HEADHEAD requestsFall back to requests.getrequests.get
Only the final URL shownIgnored response.historyresponse.historyBuild the chain from history + [r.url]history + [r.url]
SSLErrorSSLError on valid sitesOutdated certificatesUpdate certificertifi; never disable verification in production
UI freezes during batchNetwork calls on the main threadRun expansion in a background thread
Crash on malformed inputNo URL validationCheck scheme with urlparseurlparse before requesting

Variations to Try

  1. Clipboard integration — auto-read the clipboard and expand on launch.
  2. Browser extension companion — expose the logic as a small Flask API.
  3. QR support — decode a QR image to a short URL, then expand it.
  4. History log — save every expansion to a JSON file with a timestamp.
  5. Threaded UI — keep the window responsive during slow lookups.
  6. Reputation lookup — query a URL-reputation API and show a verdict.
  7. CLI mode — accept a URL as a command-line argument for scripting.

Real-World Applications

  • Security & anti-phishing — verify links before clicking in email or chat.
  • Social media tooling — audit campaign links and tracking parameters.
  • Link analytics — uncover the tracking domains a redirect passes through.
  • Content moderation — inspect user-submitted short links at scale.

Educational Value

  • HTTP fundamentals — redirects, status codes, HEADHEAD vs. GETGET.
  • Robust networking — timeouts, exception hierarchies, retries.
  • Security thinking — why obscured destinations are a risk.
  • Responsive GUIs — moving slow work off the main thread.

Next Steps

  • Print the full redirect chain instead of just the destination.
  • Add a timeout and HEADHEADGETGET fallback.
  • Implement the suspicious-link heuristics.
  • Build batch mode with a results panel and a saved history log.

Conclusion

You built a URL expander from a single requests.headrequests.head call and grew it into a redirect auditor that reveals every hop, flags risky links, survives slow servers, and processes links in bulk. Underneath a tiny UI sits a genuinely useful security habit: know where a link goes before you go there. Full source on GitHub. Find more networking projects on Python Central Hub.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did