In this tutorial, weβll build a desktop app using Tkinter that:
Formats phone numbers correctly π
Detects country and region π
Shows example formats
Saves a history of formatted numbers
Exports results to a .txt file
Supports Dark Mode π
Weβll go step by step and explain why each part exists.
π§± Step 1: Install Required Libraries
Before writing any code, install the dependencies:
pip install phonenumbers pycountry sv-ttk
What these do:
phonenumbers β validates and formats phone numbers
pycountry β converts country codes to names
sv-ttk β modern light/dark themes for Tkinter
π¦ Step 2: Import Everything We Need
Start by importing Pythonβs built-in modules and third-party libraries.
import sys
import os
import threading
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
Now the external libraries:
import sv_ttk
import phonenumbers
from phonenumbers import (
NumberParseException,
PhoneNumberFormat,
PhoneNumberType
)
import pycountry
π Step 3: Helper Functions
These small utility functions keep the main code clean.
π Access bundled files (for EXE builds)
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 helps when packaging the app with PyInstaller.
π’ Status bar updater
def set_status(msg):
status_var.set(msg)
root.update_idletasks()
This updates the UI instantly when long tasks run.
π Convert country code β country name
def get_country_name(code):
try:
return pycountry.countries.get(alpha_2=code).name
except:
return code
π Show an example phone format per country
def get_example_number(region_code):
try:
example = phonenumbers.example_number_for_type(
region_code,
PhoneNumberType.MOBILE
)
if example:
return phonenumbers.format_number(
example,
PhoneNumberFormat.NATIONAL
)
return ""
except:
return ""
πͺ Step 4: Create the Main Window
root = tk.Tk()
root.title("Universal Phone Formatter")
root.geometry("780x750")
Apply a modern theme:
sv_ttk.set_theme("light")
π Step 5: Global State Variables
Tkinter uses special variables that auto-update the UI.
dark_mode_var = tk.BooleanVar(value=False)
phone_var = tk.StringVar()
formatted_result_var = tk.StringVar(value="Result: β")
region_var = tk.StringVar(value="US")
region_example_var = tk.StringVar(value="")
phone_history = []
π Step 6: Dark Mode Toggle
def toggle_theme():
bg = "#2E2E2E" if dark_mode_var.get() else "#FFFFFF"
fg = "white" if dark_mode_var.get() else "black"
root.configure(bg=bg)
for w in [
"TFrame",
"TLabel",
"TLabelframe",
"TLabelframe.Label",
"TCheckbutton"
]:
style.configure(w, background=bg, foreground=fg)
phone_entry.configure(background=bg, foreground=fg)
region_entry.configure(background=bg, foreground=fg)
π§ Step 7: Auto-Update Example Number
Whenever the user changes the region, we update the example.
def update_region_example(*args):
region = region_var.get().strip().upper()
example = get_example_number(region)
if example:
region_example_var.set(f"Example format for {region}: {example}")
else:
region_example_var.set("No example available")
Listen for changes:
region_var.trace_add("write", update_region_example)
update_region_example()
π Step 8: Phone Formatting Logic
This is the core logic of the app.
def format_phone_number(number, region=None):
try:
parsed = phonenumbers.parse(number, region)
if not phonenumbers.is_possible_number(parsed):
return None
if not phonenumbers.is_valid_number(parsed):
return None
international = phonenumbers.format_number(
parsed, PhoneNumberFormat.INTERNATIONAL
)
national = phonenumbers.format_number(
parsed, PhoneNumberFormat.NATIONAL
)
country = get_country_name(
phonenumbers.region_code_for_number(parsed)
)
return f"{international} (National: {national}, Country: {country})"
except NumberParseException:
return None
π§΅ Step 9: Run Validation in a Background Thread
This keeps the UI responsive.
def validate_phone():
number = phone_var.get().strip()
region = region_var.get().strip().upper()
if not number:
messagebox.showwarning("Error", "Please enter a phone number.")
return
set_status("Formatting phone number...")
threading.Thread(
target=_validate_phone_thread,
args=(number, region),
daemon=True
).start()
Worker thread:
def _validate_phone_thread(number, region):
formatted = format_phone_number(number, region)
if formatted:
formatted_result_var.set(f"Result: β
{formatted}")
add_to_history(formatted)
else:
formatted_result_var.set("Result: β Invalid phone number")
set_status("Formatting complete")
π Step 10: Phone History + Export
Add to history:
def add_to_history(formatted_number):
phone_history.append(formatted_number)
history_list.insert(
tk.END,
f"{len(phone_history)} β’ {formatted_number}"
)
Export to .txt:
def export_history_txt():
if not phone_history:
messagebox.showinfo("Empty History", "No phone numbers to export.")
return
file_path = filedialog.asksaveasfilename(
defaultextension=".txt",
filetypes=[("Text Files", "*.txt")]
)
if not file_path:
return
with open(file_path, "w", encoding="utf-8") as f:
f.write("Universal Phone Number History\n")
f.write("=" * 50 + "\n\n")
for i, number in enumerate(phone_history, 1):
f.write(f"{i}. {number}\n")
messagebox.showinfo("Export Successful", "History saved!")
π¨ Step 11: Styling & Status Bar
style = ttk.Style()
style.theme_use("clam")
style.configure("Action.TButton", font=("Segoe UI", 11, "bold"), padding=8)
Status bar:
status_var = tk.StringVar(value="Ready")
ttk.Label(root, textvariable=status_var, anchor="w").pack(
side=tk.BOTTOM, fill="x"
)
π§© Step 12: Build the UI Layout
Main container:
main = ttk.Frame(root, padding=20)
main.pack(expand=True, fill="both")
Title:
ttk.Label(
main,
text="Universal Phone Formatter",
font=("Segoe UI", 22, "bold")
).pack()
Inputs, buttons, history list, and dark mode checkbox follow exactly as in your original code.
βΆοΈ Step 13: Run the App
root.mainloop()
π Final Result
You now have a fully featured desktop phone formatter that:
β
Validates real numbers
β
Detects country & format
β
Shows examples
β
Saves history
β
Exports results
β
Supports dark mode

Top comments (0)