URL Expander
Abstract
Section titled “Abstract”Shortened links (bit.ly, t.co, tinyurl) 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 requests 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
requestsfollows them. - The difference between a
HEADand aGETrequest, and whyHEADis cheaper here. - How to handle network errors without crashing the UI.
- Why expanding a link is a security practice, not just a convenience.
Prerequisites
Section titled “Prerequisites”- Python 3.6 or above.
- A text editor or IDE.
- The
requestslibrary:pip install requests. - Tkinter (bundled with Python).
- A basic grasp of HTTP requests and responses.
Getting Started
Section titled “Getting Started”Create the project
Section titled “Create the project”- Create a folder named
url-expander. - Inside it, create
url_expander.py. - Install the dependency:
pip install requests.
Write the code
Section titled “Write the code”url_expander.py
pch.viewSource"""
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
Section titled “Run it”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.How it fits together
Section titled “How it fits together”Read from the top: this is what runs when you execute the file, and which function calls which. It is generated from the code, so it cannot drift from it.
flowchart TD
RUN(["python url_expander.py"])
expand_url("expand_url")
URLExpanderApp["URLExpanderApp
class"]
main("main")
RUN --> main
URLExpanderApp --> expand_url
main --> URLExpanderApp
Step-by-Step Explanation
Section titled “Step-by-Step Explanation”1. The core: follow the redirects
Section titled “1. The core: follow the redirects”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.headasks 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=Truetellsrequeststo chase everyLocationheader automatically. After the last hop,response.urlholds the real destination.
Catching requests.RequestException (the base class for all requests errors) means a dead link or DNS failure returns a message instead of crashing.
2. Input guard
Section titled “2. Input guard”short_url = self.url_entry.get()
if not short_url:
messagebox.showerror("Error", "Please enter a URL.")
returnNever trust an empty field. Bail early with a clear message.
3. Show the result
Section titled “3. Show the result”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
Section titled “Upgrade: Show the Full Redirect Chain”A single destination hides intermediate hops — and those hops are where shady redirectors live. response.history records every step:
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.com tells you far more than final.com alone.
Add a Timeout (Always)
Section titled “Add a Timeout (Always)”A request with no timeout can hang forever, freezing your app:
requests.head(short_url, allow_redirects=True, timeout=10)Rule of thumb: every network call in production code gets a timeout. Catch requests.Timeout separately to tell the user “the server is slow” rather than “the URL is broken”.
Flag Suspicious Links
Section titled “Flag Suspicious Links”A lightweight safety check before the user clicks:
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 flagsThis is heuristic, not a malware scanner — but “credentials in URL” and “nested redirect” catch a surprising number of phishing tricks.
Batch Mode
Section titled “Batch Mode”Expand a whole list at once and show results in a Text widget instead of popups:
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
Section titled “Common Mistakes”| Problem | Cause | Fix |
|---|---|---|
| App hangs on a bad link | No timeout set | Pass timeout=10 to every request |
| Some shorteners don’t expand | They block HEAD requests | Fall back to requests.get |
| Only the final URL shown | Ignored response.history | Build the chain from history + [r.url] |
SSLError on valid sites | Outdated certificates | Update certifi; never disable verification in production |
| UI freezes during batch | Network calls on the main thread | Run expansion in a background thread |
| Crash on malformed input | No URL validation | Check scheme with urlparse before requesting |
Variations to Try
Section titled “Variations to Try”- Clipboard integration — auto-read the clipboard and expand on launch.
- Browser extension companion — expose the logic as a small Flask API.
- QR support — decode a QR image to a short URL, then expand it.
- History log — save every expansion to a JSON file with a timestamp.
- Threaded UI — keep the window responsive during slow lookups.
- Reputation lookup — query a URL-reputation API and show a verdict.
- CLI mode — accept a URL as a command-line argument for scripting.
Real-World Applications
Section titled “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
Section titled “Educational Value”- HTTP fundamentals — redirects, status codes,
HEADvs.GET. - 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
Section titled “Next Steps”- Print the full redirect chain instead of just the destination.
- Add a timeout and
HEAD→GETfallback. - Implement the suspicious-link heuristics.
- Build batch mode with a results panel and a saved history log.
Conclusion
Section titled “Conclusion”You built a URL expander from a single requests.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.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading