In this tutorial, we’ll build CurrencyTool Pro, a desktop currency converter with live and offline mode, manual rate editing, and a modern UI using Python.
You’ll learn to:
Fetch live currency rates from an API
Store and load rates locally
Build a responsive Tkinter GUI with ttk and sv_ttk
Use threads to prevent UI freezing
1️⃣ Setup and Requirements
Requirements:
Python 3.10+
tkinter (built-in)
requests → pip install requests
sv_ttk → pip install sv_ttk
2️⃣ App Initialization
Start by importing libraries and setting up the main window:
import sys, os, json, threading, tkinter as tk
from tkinter import ttk, messagebox
import sv_ttk, requests
root = tk.Tk()
root.title("CurrencyTool Pro")
root.geometry("980x620")
root.minsize(900, 550)
sv_ttk.set_theme("light")
Here we use sv_ttk for a modern light/dark theme and set the window size.
3️⃣ Global Variables and Defaults
Define variables for amount, currencies, results, and status. Also, add default rates as fallback:
amount_var = tk.DoubleVar(value=100.0)
from_var = tk.StringVar(value="USD")
to_var = tk.StringVar(value="EUR")
result_var = tk.StringVar(value="—")
status_var = tk.StringVar(value="Ready")
mode_var = tk.StringVar(value="Offline")
RATES = {
"USD": 1.0,
"EUR": 0.92,
"GBP": 0.79,
"JPY": 157.0,
"AUD": 1.52,
"CAD": 1.36,
"CHF": 0.88,
"CNY": 7.18
}
DATA_FILE = "rates_live.json"
4️⃣ Persistence: Load & Save Rates
We want offline support by saving rates locally:
def load_rates():
if os.path.exists(DATA_FILE):
try:
with open(DATA_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
RATES.clear()
RATES.update(data)
RATES["USD"] = 1.0
mode_var.set("Online")
except Exception:
pass
def save_rates():
with open(DATA_FILE, "w", encoding="utf-8") as f:
json.dump(RATES, f, indent=2)
This ensures rates persist between sessions.
5️⃣ Status Helper
Update a status bar dynamically:
def set_status(msg):
status_var.set(msg)
root.update_idletasks()
6️⃣ Fetch Live Rates (Threaded)
Fetch rates from a free API without freezing the GUI:
def fetch_live_rates():
update_btn.config(state="disabled")
set_status("🌐 Fetching live rates...")
try:
r = requests.get("https://open.er-api.com/v6/latest/USD", timeout=6)
data = r.json()
if data.get("result") != "success":
raise RuntimeError
RATES.clear()
RATES.update(data["rates"])
RATES["USD"] = 1.0
save_rates()
refresh_currency_lists()
mode_var.set("Online")
set_status("🌐 Live rates updated & saved")
except Exception:
messagebox.showwarning("Live Update Failed", "Unable to fetch live rates. Using offline data.")
set_status("Offline mode")
finally:
update_btn.config(state="normal")
Use threading.Thread when connecting this to a button to prevent UI freeze.
7️⃣ Currency Conversion Logic
Convert an amount from one currency to another:
def convert_currency():
try:
amount = amount_var.get()
f, t = from_var.get(), to_var.get()
usd = amount / RATES[f]
result = usd * RATES[t]
result_var.set(f"{result:,.4f} {t}")
set_status(f"Converted ({mode_var.get()})")
except Exception:
messagebox.showerror("Error", "Invalid amount or currency.")
def swap_currencies():
f, t = from_var.get(), to_var.get()
from_var.set(t)
to_var.set(f)
8️⃣ Manual Rate Editor
Users can override rates manually in offline mode:
def open_rate_editor():
editor = tk.Toplevel(root)
editor.title("✏️ Manual Rate Override (USD Base)")
editor.geometry("420x520")
editor.transient(root)
editor.grab_set()
frame = ttk.Frame(editor, padding=16)
frame.pack(fill="both", expand=True)
ttk.Label(frame, text="Manual Rate Override", font=("Segoe UI", 14, "bold")).pack(anchor="w")
canvas = tk.Canvas(frame)
scrollbar = ttk.Scrollbar(frame, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
canvas.create_window((0,0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
entries = {}
for c in sorted(RATES.keys()):
row = ttk.Frame(scrollable_frame)
row.pack(fill="x", pady=2)
ttk.Label(row, text=c, width=6).pack(side="left")
v = tk.DoubleVar(value=RATES[c])
ttk.Entry(row, textvariable=v, width=12).pack(side="left", padx=4)
entries[c] = v
ttk.Button(row, text="💾", width=3, command=lambda c=c: save_single_rate(c), style="Action.TButton").pack(side="right")
def save_single_rate(currency):
try:
RATES[currency] = float(entries[currency].get())
save_rates()
refresh_currency_lists()
mode_var.set("Offline")
set_status(f"💾 {currency} rate saved")
except Exception:
messagebox.showerror("Invalid Input", "Rate must be numeric.")
9️⃣ Refresh UI Currency Lists
Update dropdown lists whenever rates change:
def refresh_currency_lists():
cur = sorted(RATES.keys())
from_combo["values"] = cur
to_combo["values"] = cur
🔟 Build the GUI Layout
Use ttk frames, labels, buttons, and comboboxes. Here's the converter card:
card = ttk.LabelFrame(main, text="Converter", padding=20)
card.grid(row=0, column=0, sticky="ew")
for i in range(4): card.columnconfigure(i, weight=1)
ttk.Label(card, text="Amount").grid(row=0, column=0, sticky="w")
ttk.Entry(card, textvariable=amount_var, font=("Segoe UI",11)).grid(row=1, column=0, sticky="ew")
ttk.Label(card, text="From").grid(row=0, column=1, sticky="w")
from_combo = ttk.Combobox(card, textvariable=from_var, state="readonly")
from_combo.grid(row=1, column=1, sticky="ew")
ttk.Label(card, text="To").grid(row=0, column=2, sticky="w")
to_combo = ttk.Combobox(card, textvariable=to_var, state="readonly")
to_combo.grid(row=1, column=2, sticky="ew")
ttk.Button(card, text="⇄", command=swap_currencies).grid(row=1, column=3, sticky="ew")
ttk.Label(card, text="Result").grid(row=2, column=0, columnspan=4, sticky="w")
ttk.Label(card, textvariable=result_var).grid(row=3, column=0, columnspan=4, sticky="w")
You can follow the full layout from the previous complete source code for headers, status bar, and buttons.
1️⃣1️⃣ Run the App
Finally, initialize rates and run the main loop:
load_rates()
refresh_currency_lists()
root.mainloop()
✅ Features Recap
Live fetching & offline fallback
Threaded requests for smooth UI
Manual rate override
Modern, responsive Tkinter GUI

Top comments (0)