In this tutorial, weβll build a local URL shortener desktop app using Python + Tkinter.
No internet. No backend. Everything is stored locally in a JSON file.
By the end, youβll have:
A clean GUI
URL shortening & expanding
Clipboard support
Dark mode
Persistent local storage
π§° Requirements
Before we start, install the required packages:
pip install sv-ttk
Tkinter comes bundled with Python, so no extra install needed.
π¦ Step 1: Import Required Modules
Weβll start by importing everything our app needs.
import sys
import os
import json
import tkinter as tk
from tkinter import ttk, messagebox
import sv_ttk
import random
What each module does:
tkinter / ttk β GUI widgets
messagebox β pop-up alerts
json β store URLs locally
random β generate short codes
sv_ttk β modern UI theme
os / sys β file path handling
π§ Step 2: Helper Functions
π Handle file paths safely (important for packaging)
def resource_path(file_name):
base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base_path, file_name)
This ensures your app works both:
normally
when bundled with PyInstaller
π’ Update the status bar
def set_status(msg):
status_var.set(msg)
root.update_idletasks()
This gives users real-time feedback (e.g. βURL shortened successfullyβ).
π² Generate a simple short code
def generate_easy_code():
prefixes = ["go", "url", "link", "up", "web"]
prefix = random.choice(prefixes)
number = random.randint(10, 999)
return f"{prefix}{number}"
Example output:
go123
link88
web402
Simple and readable for beginners.
πΎ Step 3: Local Storage with JSON
π Define storage file
STORAGE_FILE = resource_path("short_urls.json")
π Load existing data (if any)
if os.path.exists(STORAGE_FILE):
with open(STORAGE_FILE, "r", encoding="utf-8") as f:
url_mapping = json.load(f)
else:
url_mapping = {}
This keeps your shortened URLs even after closing the app.
πΎ Save mappings to disk
def save_mapping():
with open(STORAGE_FILE, "w", encoding="utf-8") as f:
json.dump(url_mapping, f, indent=4)
update_url_list()
π Step 4: Core URL Logic
βοΈ Shorten a URL
def shorten_url():
original = url_var.get().strip()
We first read the input field and clean whitespace.
β οΈ Validate input
if not original:
messagebox.showwarning("Input Required", "Enter a URL to shorten.")
return
β»οΈ Avoid duplicates
for code, url in url_mapping.items():
if url == original:
short_var.set(code)
set_status("URL already shortened.")
return
Same URL = same short code.
π Generate a new code
code = generate_easy_code()
while code in url_mapping:
code = generate_easy_code()
Ensures uniqueness.
πΎ Save result
url_mapping[code] = original
save_mapping()
short_var.set(code)
set_status("URL shortened successfully.")
π Expand a short code
def expand_url():
code = short_var.get().strip()
if not code:
messagebox.showwarning("Input Required", "Enter a short code to expand.")
return
original = url_mapping.get(code)
if original:
url_var.set(original)
set_status("URL expanded successfully.")
else:
messagebox.showerror("Not Found", "Short code does not exist.")
π Step 5: Clipboard Support
def copy_to_clipboard(entry_var):
root.clipboard_clear()
root.clipboard_append(entry_var.get())
set_status("Copied to clipboard!")
Works for both URLs and short codes.
π Step 6: Display Stored URLs
def update_url_list():
for widget in list_frame_inner.winfo_children():
widget.destroy()
Clears old UI elements before refreshing.
for i, (code, url) in enumerate(url_mapping.items(), 1):
ttk.Label(
list_frame_inner,
text=f"{i}. {code} β {url}",
font=("Segoe UI", 10),
anchor="w"
).pack(fill="x", padx=5, pady=2)
Creates a clean numbered list.
π¨ Step 7: App Setup & Theme
πͺ Create the window
root = tk.Tk()
root.title("Local URL Shortener")
root.geometry("1100x650")
sv_ttk.set_theme("light")
π Global variables
url_var = tk.StringVar()
short_var = tk.StringVar()
dark_mode_var = tk.BooleanVar(value=False)
π Dark mode toggle
def toggle_theme():
style.theme_use("clam")
bg = "#2E2E2E" if dark_mode_var.get() else "#FFFFFF"
fg = "white" if dark_mode_var.get() else "black"
root.configure(bg=bg)
for entry in [url_entry, short_entry]:
entry.configure(
bg="#1e1e1e" if dark_mode_var.get() else "white",
fg="white" if dark_mode_var.get() else "black",
insertbackground="white" if dark_mode_var.get() else "black"
)
ποΈ Step 8: UI Layout
π Status bar
status_var = tk.StringVar(value="Ready")
ttk.Label(root, textvariable=status_var, anchor="w").pack(side=tk.BOTTOM, fill="x")
π§± Main container
main = ttk.Frame(root, padding=20)
main.pack(expand=True, fill="both")
π·οΈ Input fields
url_entry = tk.Entry(url_frame, textvariable=url_var, font=("Segoe UI", 12))
short_entry = tk.Entry(short_frame, textvariable=short_var, font=("Segoe UI", 12))
π― Action buttons
ttk.Button(actions, text="π Shorten", command=shorten_url).pack(side="left")
ttk.Button(actions, text="π Expand", command=expand_url).pack(side="left")
ttk.Button(actions, text="π Copy URL", command=lambda: copy_to_clipboard(url_var)).pack(side="left")
ttk.Button(actions, text="π Copy Code", command=lambda: copy_to_clipboard(short_var)).pack(side="left")
π Step 9: Scrollable Stored URLs List
We use a Canvas + Frame + Scrollbar combo:
list_frame_canvas = tk.Canvas(list_frame_outer)
scrollbar = ttk.Scrollbar(list_frame_outer, orient="vertical", command=list_frame_canvas.yview)
list_frame_inner = ttk.Frame(list_frame_canvas)
list_frame_canvas.create_window((0, 0), window=list_frame_inner, anchor="nw")
def on_frame_configure(event):
list_frame_canvas.configure(scrollregion=list_frame_canvas.bbox("all"))
βΆοΈ Step 10: Run the App
update_url_list()
root.mainloop()
Your app is now live π
β Final Features Recap
π Local URL shortening
π URL expansion
πΎ Persistent JSON storage
π Dark mode toggle
π Clipboard support
π₯οΈ Desktop GUI (no web server)

Top comments (0)