Simple Stopwatch
Abstract
Section titled “Abstract”A stopwatch is a deceptively small project — under 50 lines and you have a working app. Behind those lines sit two important Tkinter concepts: the event loop and scheduled callbacks via after. We will start with the naïve counter-variable version (which drifts), fix it with time.time() for true elapsed-time accuracy, then add HH:MM:SS formatting, lap times, keyboard shortcuts, persistence between sessions, and a countdown mode.
You will leave understanding:
- Why a recurring
after(1000, fn)is not a real 1-second timer. - The right way to measure elapsed time (record a start timestamp; subtract from
time.time()). - How to format seconds as
HH:MM:SS. - The lap-time pattern.
- How to bind keyboard shortcuts to Tkinter buttons.
Prerequisites
Section titled “Prerequisites”- Python 3.6 or above.
- A text editor or IDE.
- Tkinter (ships with Python; Linux:
sudo apt install python3-tk). - Familiarity with the Tkinter event loop. Basic Music Player is a softer first GUI project.
Getting Started
Section titled “Getting Started”Create the project
Section titled “Create the project”- Create folder
simple-stopwatch. - Inside, create
simplestopwatch.py.
Write the code
Section titled “Write the code”Simple Stopwatch
pch.viewSource# Simple Stopwatch
# Import Modules
import time
import datetime
import tkinter as tk
# Defining Variables
root = tk.Tk()
root.title("Simple Stopwatch")
root.geometry("500x500")
root.resizable(False, False)
root.config(bg="black")
# Defining Functions
def start():
global running
running = True
global count
count = -1
counter()
def counter():
global running
global count
if running:
count += 1
time_label.config(text=str(count))
time_label.after(1000, counter)
def stop():
global running
running = False
def reset():
global count
count = 0
time_label.config(text=str(count))
# Creating Widgets
time_label = tk.Label(root, text="0", font=("Helvetica", 80), bg="black", fg="white")
start_button = tk.Button(root, text="Start", font=("Helvetica", 20), bg="black", fg="white", command=start)
stop_button = tk.Button(root, text="Stop", font=("Helvetica", 20), bg="black", fg="white", command=stop)
reset_button = tk.Button(root, text="Reset", font=("Helvetica", 20), bg="black", fg="white", command=reset)
# Placing Widgets
time_label.pack(pady=20)
start_button.pack(pady=20)
stop_button.pack(pady=20)
reset_button.pack(pady=20)
# Calling Functions
root.mainloop() Run it
Section titled “Run it”C:\Users\Your Name\simple-stopwatch> python simplestopwatch.py
# Window with big "0", Start / Stop / Reset buttonsHow it fits together
Section titled “How it fits together”Read from the top: this is what runs when you execute the file, and which function calls which. It is generated from the code, so it cannot drift from it.
flowchart TD
RUN(["python simplestopwatch.py"])
start("start")
counter("counter")
stop("stop")
reset("reset")
RUN --> start
start --> counter
Step-by-Step Explanation
Section titled “Step-by-Step Explanation”1. Imports and root window
Section titled “1. Imports and root window”import tkinter as tk
root = tk.Tk()
root.title("Simple Stopwatch")
root.geometry("400x300")
root.config(bg="black")2. State and label
Section titled “2. State and label”running = False
count = 0
label = tk.Label(root, text="0", font=("Helvetica", 80), bg="black", fg="white")
label.pack(pady=20)3. The naïve counter loop
Section titled “3. The naïve counter loop”def tick():
global count
if running:
count += 1
label.config(text=str(count))
label.after(1000, tick) # schedule the next call
def start():
global running
if not running:
running = True
tick()
def stop():
global running
running = False
def reset():
global count
count = 0
label.config(text="0")after(ms, fn) tells Tkinter “call fn after ms milliseconds during the event loop.” That is how you do “do something every second” without blocking the UI.
4. Wire the buttons
Section titled “4. Wire the buttons”for txt, cmd in [("Start", start), ("Stop", stop), ("Reset", reset)]:
tk.Button(root, text=txt, font=("Helvetica", 18),
bg="black", fg="white", command=cmd).pack(pady=10)
root.mainloop()The Drift Problem
Section titled “The Drift Problem”The naïve version is wrong. Two reasons:
- Tkinter’s
afteris not a precise scheduler. A second’s worth of wall-clock might be 1.01s on a busy laptop. After a minute you have drifted by half a second. countmeasures “ticks fired”, not elapsed time. If the UI is sluggish, ticks pile up — or skip.
Fix it by recording a start timestamp and computing elapsed time from time.time():
import time
start_time = 0.0
elapsed_at_pause = 0.0
running = False
def tick():
if running:
elapsed = (time.time() - start_time) + elapsed_at_pause
label.config(text=format_time(elapsed))
label.after(50, tick) # 50 ms = 20 fps; smooth display, irrelevant to accuracy
def start():
global running, start_time
if not running:
running = True
start_time = time.time()
tick()
def stop():
global running, elapsed_at_pause
if running:
elapsed_at_pause += time.time() - start_time
running = False
def reset():
global running, elapsed_at_pause
running = False
elapsed_at_pause = 0
label.config(text="00:00.0")Now the displayed time is always accurate to within one frame, regardless of how laggy the UI is.
Format Time as HH:MM:SS.t
Section titled “Format Time as HH:MM:SS.t”def format_time(seconds: float) -> str:
h, rem = divmod(int(seconds), 3600)
m, s = divmod(rem, 60)
tenths = int((seconds - int(seconds)) * 10)
return f"{h:02d}:{m:02d}:{s:02d}.{tenths}"divmod(a, b) returns (a // b, a % b) in one call — the standard Python idiom for time-component math.
Lap Times
Section titled “Lap Times”A lap button records the current elapsed time without resetting:
laps: list[float] = []
laps_list = tk.Listbox(root, height=8)
laps_list.pack(fill="x", padx=20)
def add_lap():
if not running: return
elapsed = (time.time() - start_time) + elapsed_at_pause
laps.append(elapsed)
n = len(laps)
diff = elapsed - (laps[-2] if n > 1 else 0)
laps_list.insert("end", f"Lap {n}: {format_time(elapsed)} (+{format_time(diff)})")
tk.Button(root, text="Lap", command=add_lap).pack(pady=4)Each entry shows both the total elapsed and the split from the previous lap.
Keyboard Shortcuts
Section titled “Keyboard Shortcuts”Make Space toggle, R reset, L lap:
def toggle(_event=None):
stop() if running else start()
root.bind("<space>", toggle)
root.bind("<r>", lambda e: reset())
root.bind("<l>", lambda e: add_lap())Common Mistakes
Section titled “Common Mistakes”| Problem | Cause | Fix |
|---|---|---|
| Drift after 5 minutes | Counted ticks, not elapsed time | Use time.time() reference |
Multiple tick() chains spawn after rapid Start clicks | No guard on start() | if not running: before scheduling |
| Window freezes on heavy work | Did blocking work in callback | Keep callbacks fast; use threads for blocking ops |
| Reset while running leaves stale tick scheduled | Did not stop the chain | Set running = False before resetting |
| Tenths jitter visibly | Refreshed every 1000 ms | Refresh every 50–100 ms; format with fractional seconds |
| Numbers misaligned | Variable-width font | Use a monospace font (Consolas, Courier) |
Variations to Try
Section titled “Variations to Try”1. Countdown timer
Section titled “1. Countdown timer”duration = 5 * 60 # 5 minutes
def tick():
remaining = duration - ((time.time() - start_time) + elapsed_at_pause)
if remaining <= 0:
label.config(text="00:00.0")
notify("Time!")
return
label.config(text=format_time(remaining))
label.after(50, tick)Combines neatly with Basic Alarm Clock.
2. Pomodoro mode
Section titled “2. Pomodoro mode”25 minutes of work, 5 minutes break, repeat. Toggle a phase state and chain timers.
3. Save lap history
Section titled “3. Save lap history”JSON file with timestamp + laps list. Reload on next launch.
4. CSV export
Section titled “4. CSV export”“Export laps” button writes to laps.csv.
5. Multiple stopwatches
Section titled “5. Multiple stopwatches”A Tab per stopwatch using ttk.Notebook. Each tab keeps its own state.
6. Color states
Section titled “6. Color states”Green while running, red while stopped, blue at zero. Visual at a glance.
7. Sound on lap / finish
Section titled “7. Sound on lap / finish”winsound on Windows, playsound cross-platform. See Basic Music Player.
8. Voice commands
Section titled “8. Voice commands”pip install SpeechRecognition — say “start”, “stop”, “lap”, “reset”.
9. Race mode
Section titled “9. Race mode”Two stopwatches in one window, each with its own start/stop button. Show who is ahead.
10. Web stopwatch
Section titled “10. Web stopwatch”Flask plus a tiny page that updates with JS. The server keeps state per session.
Class-Based Refactor
Section titled “Class-Based Refactor”Globals are fine here, but if you build the multi-stopwatch variant you want state per instance:
class Stopwatch:
def __init__(self, parent):
self.running = False
self.elapsed = 0.0
self.start_time = 0.0
self.label = tk.Label(parent, text="00:00.0",
font=("Consolas", 60), bg="black", fg="white")
self.label.pack()
def start(self):
if not self.running:
self.running = True
self.start_time = time.time()
self._tick()
def _tick(self):
if not self.running: return
now_elapsed = (time.time() - self.start_time) + self.elapsed
self.label.config(text=format_time(now_elapsed))
self.label.after(50, self._tick)
def stop(self):
if self.running:
self.elapsed += time.time() - self.start_time
self.running = False
def reset(self):
self.running = False
self.elapsed = 0
self.label.config(text="00:00.0")Real-World Applications
Section titled “Real-World Applications”- Workout / interval training.
- Cooking — multi-timer for parallel dishes.
- Productivity — Pomodoro and time-tracking.
- Sports — race day timing.
- Lab experiments — recording multiple event durations.
- Billing — track time on tasks.
Educational Value
Section titled “Educational Value”- Tkinter event loop &
after— the cornerstone of any GUI animation. - Time accuracy — counters drift, timestamps do not.
- State management —
running,start_time,elapsed_at_pause. - Formatting —
divmodis the right tool for time math. - Keyboard bindings — usability beyond mouse clicks.
Next Steps
Section titled “Next Steps”- Rewrite using
time.time()for accuracy. - Add
HH:MM:SS.tdisplay. - Add lap times with a Listbox.
- Add keyboard shortcuts.
- Refactor into a
Stopwatchclass and host two of them in one window. - Pair with Basic Alarm Clock for a countdown / Pomodoro hybrid.
Conclusion
Section titled “Conclusion”You built a real desktop stopwatch, then fixed the most common time-tracking bug (drift from counting ticks). The same lesson — measure moments, not steps — applies to nearly every time-sensitive program you will ever write. Full source on GitHub. More GUI projects on Python Central Hub.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading