DEV Community

Mate Technologies
Mate Technologies

Posted on

🧰 Building a Universal Phone Formatter GUI in Python (Beginner-Friendly)

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

Now the external libraries:

import sv_ttk
import phonenumbers
from phonenumbers import (
    NumberParseException,
    PhoneNumberFormat,
    PhoneNumberType
)
import pycountry
Enter fullscreen mode Exit fullscreen mode

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

This helps when packaging the app with PyInstaller.

πŸ“’ Status bar updater

def set_status(msg):
    status_var.set(msg)
    root.update_idletasks()
Enter fullscreen mode Exit fullscreen mode

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

πŸ“ž 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 ""
Enter fullscreen mode Exit fullscreen mode

πŸͺŸ Step 4: Create the Main Window

root = tk.Tk()
root.title("Universal Phone Formatter")
root.geometry("780x750")
Enter fullscreen mode Exit fullscreen mode

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

πŸŒ— 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)
Enter fullscreen mode Exit fullscreen mode

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

Listen for changes:

region_var.trace_add("write", update_region_example)
update_region_example()
Enter fullscreen mode Exit fullscreen mode

πŸ” 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
Enter fullscreen mode Exit fullscreen mode

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

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

πŸ—‚ 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}"
    )

Enter fullscreen mode Exit fullscreen mode

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

🎨 Step 11: Styling & Status Bar

style = ttk.Style()
style.theme_use("clam")
style.configure("Action.TButton", font=("Segoe UI", 11, "bold"), padding=8)
Enter fullscreen mode Exit fullscreen mode

Status bar:

status_var = tk.StringVar(value="Ready")
ttk.Label(root, textvariable=status_var, anchor="w").pack(
    side=tk.BOTTOM, fill="x"
)
Enter fullscreen mode Exit fullscreen mode

🧩 Step 12: Build the UI Layout

Main container:

main = ttk.Frame(root, padding=20)
main.pack(expand=True, fill="both")
Enter fullscreen mode Exit fullscreen mode

Title:

ttk.Label(
    main,
    text="Universal Phone Formatter",
    font=("Segoe UI", 22, "bold")
).pack()
Enter fullscreen mode Exit fullscreen mode

Inputs, buttons, history list, and dark mode checkbox follow exactly as in your original code.

▢️ Step 13: Run the App

root.mainloop()
Enter fullscreen mode Exit fullscreen mode

πŸŽ‰ 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

Universal Phone Formatter

Top comments (0)