DEV Community

Mate Technologies
Mate Technologies

Posted on

Build a Live + Offline Currency Converter in Python with Tkinter

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")
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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()

Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.")

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

🔟 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")
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

✅ Features Recap

Live fetching & offline fallback

Threaded requests for smooth UI

Manual rate override

Modern, responsive Tkinter GUI

CurrencyTool

Top comments (0)