DEV Community

Mate Technologies
Mate Technologies

Posted on

🕒 Building a “Stopwatch Pro” App in Python (Tkinter + Threads)

In this tutorial, we’ll build a modern desktop stopwatch app in Python with:

⏱ Stopwatch mode

⏰ Countdown timer

🏁 Lap tracking

🌗 Light / Dark mode

🧵 Threaded timing (no UI freezing!)

This guide is beginner-friendly and broken into small, understandable steps.

✅ Prerequisites

Make sure you have:

Python 3.9+

Basic Python knowledge

Tkinter (included with Python)

We’ll also use:

pip install sv-ttk

🧱 Step 1: Imports and Dependencies

Let’s start by importing what we need.

import sys
import os
import time
import threading
import tkinter as tk
from tkinter import ttk, messagebox
import sv_ttk
Enter fullscreen mode Exit fullscreen mode

Why these?

time → high-precision timing

threading → run the timer without freezing the UI

tkinter → GUI framework

sv_ttk → modern themed widgets

📦 Step 2: Helper Functions
2.1 Access bundled resources safely

This helps when packaging the app later (e.g., PyInstaller).

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

2.2 Status bar updates

We’ll show live feedback to the user.

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

🪟 Step 3: Create the Main Window

root = tk.Tk()
root.title("Stopwatch Pro")
root.geometry("640x540")
Enter fullscreen mode Exit fullscreen mode

Apply a modern theme:

sv_ttk.set_theme("light")
Enter fullscreen mode Exit fullscreen mode

🌍 Step 4: Global State Variables

These variables keep track of the app’s state.

dark_mode_var = tk.BooleanVar(value=False)
countdown_mode = tk.BooleanVar(value=False)
Enter fullscreen mode Exit fullscreen mode

Timing state:

running = False
start_time = None
elapsed_time = 0.0
Enter fullscreen mode Exit fullscreen mode

Countdown settings:

countdown_seconds = tk.IntVar(value=60)
Enter fullscreen mode Exit fullscreen mode

Lap storage:

laps = []
Enter fullscreen mode Exit fullscreen mode

Displayed time string:

time_var = tk.StringVar(value="00:00:00.000")
Enter fullscreen mode Exit fullscreen mode

🌗 Step 5: Light / 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)

    time_label.configure(
        background=bg,
        foreground="#00FFAA" if dark_mode_var.get() else fg
    )

    set_status(f"Theme switched to {'Dark' if dark_mode_var.get() else 'Light'} mode")
Enter fullscreen mode Exit fullscreen mode

💡 This dynamically updates widget colors without restarting the app.

⏱ Step 6: Time Formatting Logic

We convert seconds into a stopwatch-friendly format.

def format_time(seconds):
    ms = int((seconds - int(seconds)) * 1000)
    s = int(seconds) % 60
    m = (int(seconds) // 60) % 60
    h = int(seconds) // 3600
    return f"{h:02}:{m:02}:{s:02}.{ms:03}"
Enter fullscreen mode Exit fullscreen mode

🧵 Step 7: Timer Thread Loop

This runs in the background so the UI stays responsive.

def timer_loop():
    global elapsed_time, running
    while running:
        now = time.perf_counter()
Enter fullscreen mode Exit fullscreen mode

Stopwatch mode

        if not countdown_mode.get():
            elapsed_time = now - start_time
            time_var.set(format_time(elapsed_time))
Enter fullscreen mode Exit fullscreen mode

Countdown mode

        else:
            remaining = countdown_seconds.get() - (now - start_time)
            if remaining <= 0:
                running = False
                time_var.set("00:00:00.000")
                messagebox.showinfo("⏰ Time's up", "Countdown finished!")
                set_status("Countdown completed")
                return
            time_var.set(format_time(remaining))
Enter fullscreen mode Exit fullscreen mode

Limit updates to ~100 FPS:

        time.sleep(0.01)
Enter fullscreen mode Exit fullscreen mode

▶️ Step 8: Timer Controls
Start

def start():
    global running, start_time
    if running:
        return

    running = True
    start_time = time.perf_counter() - elapsed_time
    threading.Thread(target=timer_loop, daemon=True).start()
    set_status("Timer started")
Enter fullscreen mode Exit fullscreen mode

Stop

def stop():
    global running
    running = False
    set_status("Timer stopped")
Enter fullscreen mode Exit fullscreen mode

Reset

def reset():
    global running, elapsed_time, start_time, laps
    running = False
    elapsed_time = 0.0
    start_time = None
    laps.clear()
    lap_list.delete(0, tk.END)
    time_var.set("00:00:00.000")
    set_status("Timer reset")
Enter fullscreen mode Exit fullscreen mode

Lap Recording

def add_lap():
    if not running or countdown_mode.get():
        return

    lap_time = format_time(elapsed_time)
    laps.append(elapsed_time)
    lap_list.insert(tk.END, f"Lap {len(laps)} — {lap_time}")
    set_status(f"Lap {len(laps)} recorded")
Enter fullscreen mode Exit fullscreen mode

🎨 Step 9: Styling Buttons

style = ttk.Style()
style.theme_use("clam")

style.configure(
    "Action.TButton",
    font=("Segoe UI", 11, "bold"),
    foreground="white",
    background="#4CAF50",
    padding=8
)

style.map(
    "Action.TButton",
    background=[("active", "#45a049")]
)
Enter fullscreen mode Exit fullscreen mode

📊 Step 10: 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 11: Build the UI Layout
Main container

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

Title & subtitle
ttk.Label(main, text="Stopwatch Pro",
          font=("Segoe UI", 22, "bold")).pack()

ttk.Label(main, text="Stopwatch • Countdown • Lap Timer",
          font=("Segoe UI", 11)).pack(pady=(0, 10))
Enter fullscreen mode Exit fullscreen mode

Timer display

time_label = ttk.Label(
    main,
    textvariable=time_var,
    font=("Segoe UI", 36, "bold"),
    anchor="center"
)
time_label.pack(pady=12)

Control buttons
controls = ttk.Frame(main)
controls.pack(pady=8)

ttk.Button(controls, text="▶ Start", command=start,
           style="Action.TButton").pack(side="left", padx=4)

ttk.Button(controls, text="⏸ Stop", command=stop,
           style="Action.TButton").pack(side="left", padx=4)

ttk.Button(controls, text="⟲ Reset", command=reset,
           style="Action.TButton").pack(side="left", padx=4)

ttk.Button(controls, text="🏁 Lap", command=add_lap,
           style="Action.TButton").pack(side="left", padx=4)
Enter fullscreen mode Exit fullscreen mode

⏰ Step 12: Countdown Settings

countdown_frame = ttk.LabelFrame(main, text="Countdown Mode", padding=10)
countdown_frame.pack(fill="x", pady=8)
Enter fullscreen mode Exit fullscreen mode

Enable toggle:

ttk.Checkbutton(
    countdown_frame,
    text="Enable Countdown",
    variable=countdown_mode
).pack(side="left", padx=6)
Enter fullscreen mode Exit fullscreen mode

Time input:

ttk.Label(countdown_frame, text="Seconds:").pack(side="left", padx=6)

ttk.Spinbox(
    countdown_frame,
    from_=1,
    to=86400,
    textvariable=countdown_seconds,
    width=8
).pack(side="left")
Enter fullscreen mode Exit fullscreen mode

🏁 Step 13: Lap List

laps_frame = ttk.LabelFrame(main, text="Lap Times", padding=10)
laps_frame.pack(fill="both", expand=True, pady=8)

lap_list = tk.Listbox(
    laps_frame,
    height=7,
    font=("Segoe UI", 10)
)
lap_list.pack(fill="both", expand=True)
Enter fullscreen mode Exit fullscreen mode

⚙️ Step 14: App Options

options = ttk.Frame(main)
options.pack(pady=10)

ttk.Checkbutton(
    options,
    text="Dark Mode",
    variable=dark_mode_var,
    command=toggle_theme
).pack()
Enter fullscreen mode Exit fullscreen mode

🚀 Step 15: Run the App

root.mainloop()
Enter fullscreen mode Exit fullscreen mode

🎉 Final Result

You now have a fully-featured, modern Stopwatch app with:

Smooth timing

Thread-safe UI

Countdown support

Lap tracking

Theme switching

Stopwatch Pro

Top comments (0)