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
requestsrequestsfollows them. - The difference between a
HEADHEADand aGETGETrequest, and whyHEADHEADis 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
requestsrequestslibrary:pip install requestspip install requests. - Tkinter (bundled with Python).
- A basic grasp of HTTP requests and responses.
Getting Started
Create the project
- Create a folder named
url-expanderurl-expander. - Inside it, create
url_expander.pyurl_expander.py. - Install the dependency:
pip install requestspip install requests.
Write the code
url_expander.py
Source"""
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
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
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.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
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)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.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=Trueallow_redirects=Truetellsrequestsrequeststo chase everyLocationLocationheader automatically. After the last hop,response.urlresponse.urlholds 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
short_url = self.url_entry.get()
if not short_url:
messagebox.showerror("Error", "Please enter a URL.")
returnshort_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
expanded_url = expand_url(short_url)
messagebox.showinfo("Expanded URL", f"Original URL: {expanded_url}")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:
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}")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:
requests.head(short_url, allow_redirects=True, timeout=10)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”.
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 flagsfrom 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
Expand a whole list at once and show results in a TextText 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")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
| Problem | Cause | Fix |
|---|---|---|
| App hangs on a bad link | No timeouttimeout set | Pass timeout=10timeout=10 to every request |
| Some shorteners don’t expand | They block HEADHEAD requests | Fall back to requests.getrequests.get |
| Only the final URL shown | Ignored response.historyresponse.history | Build the chain from history + [r.url]history + [r.url] |
SSLErrorSSLError on valid sites | Outdated certificates | Update certificertifi; 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 urlparseurlparse before requesting |
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
- 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,
HEADHEADvs.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
HEADHEAD→GETGETfallback. - 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 coffeeWas this page helpful?
Let us know how we did
