Python Stopwatch: perf_counter(), Laps, and Tkinter

Quick answer: A Python stopwatch should measure elapsed time with time.perf_counter(), accumulate completed running intervals, and update a Tkinter display with after() rather than blocking the GUI thread. Keep start, stop, reset, and lap transitions explicit so the clock remains correct after pauses.

Python Pool infographic showing Python stopwatch perf_counter start stop laps elapsed duration and Tkinter after loop
A reliable stopwatch measures elapsed intervals with a monotonic performance clock and keeps GUI updates non-blocking.

You can build a stopwatch in Python with the standard library. For elapsed-time measurement, use time.perf_counter(). It is designed for measuring short durations and avoids problems that can happen when the system clock changes.

This guide shows two versions: a command-line stopwatch and a simple Tkinter stopwatch GUI.

Command-line stopwatch

This version starts when the user presses Enter and stops with Ctrl+C.

from time import perf_counter

input("Press Enter to start the stopwatch...")
start = perf_counter()

try:
    while True:
        elapsed = perf_counter() - start
        print(f"Elapsed: {elapsed:0.2f} seconds", end="\r")
except KeyboardInterrupt:
    elapsed = perf_counter() - start
    print(f"\nFinal time: {elapsed:0.2f} seconds")

perf_counter() returns a clock value in fractional seconds. The absolute value is not meaningful by itself; subtract one reading from another to get elapsed time.

If you are new to terminal input, see Python user input. For the Ctrl+C behavior, see KeyboardInterrupt in Python.

Format elapsed time as minutes and seconds

For a stopwatch display, convert seconds into minutes, seconds, and hundredths.

def format_elapsed(seconds):
    minutes, seconds = divmod(seconds, 60)
    whole_seconds = int(seconds)
    hundredths = int((seconds - whole_seconds) * 100)
    return f"{int(minutes):02d}:{whole_seconds:02d}.{hundredths:02d}"

print(format_elapsed(75.42))
01:15.42

Stopwatch with lap times

A lap timer stores intermediate elapsed values without resetting the stopwatch.

from time import perf_counter

start = perf_counter()
laps = []

while True:
    command = input("Enter for lap, q to quit: ").strip().lower()

    if command == "q":
        break

    lap_time = perf_counter() - start
    laps.append(lap_time)
    print(f"Lap {len(laps)}: {lap_time:0.2f} seconds")

print("All laps:", [round(lap, 2) for lap in laps])

Python Pool infographic showing start perf_counter, code block, stop reading, and elapsed duration
perf_counter is designed for measuring short elapsed durations with a monotonic clock.

Tkinter stopwatch GUI

Tkinter is Python’s standard GUI toolkit. In a Tkinter app, avoid a blocking while True loop because it freezes the interface. Use root.after() to schedule repeated updates.

import tkinter as tk
from time import perf_counter

running = False
start_time = 0.0
elapsed_before_start = 0.0

def format_elapsed(seconds):
    minutes, seconds = divmod(seconds, 60)
    whole_seconds = int(seconds)
    hundredths = int((seconds - whole_seconds) * 100)
    return f"{int(minutes):02d}:{whole_seconds:02d}.{hundredths:02d}"

def update_display():
    if running:
        elapsed = elapsed_before_start + (perf_counter() - start_time)
        label.config(text=format_elapsed(elapsed))
    root.after(50, update_display)

def start():
    global running, start_time
    if not running:
        running = True
        start_time = perf_counter()

def stop():
    global running, elapsed_before_start
    if running:
        elapsed_before_start += perf_counter() - start_time
        running = False

def reset():
    global running, start_time, elapsed_before_start
    running = False
    start_time = 0.0
    elapsed_before_start = 0.0
    label.config(text="00:00.00")

root = tk.Tk()
root.title("Python Stopwatch")

label = tk.Label(root, text="00:00.00", font=("Arial", 36))
label.pack(padx=20, pady=20)

buttons = tk.Frame(root)
buttons.pack(pady=10)

tk.Button(buttons, text="Start", command=start).grid(row=0, column=0, padx=5)
tk.Button(buttons, text="Stop", command=stop).grid(row=0, column=1, padx=5)
tk.Button(buttons, text="Reset", command=reset).grid(row=0, column=2, padx=5)

update_display()
root.mainloop()

Run python -m tkinter in a terminal if you want to check whether Tkinter is installed correctly.

Why use perf_counter() instead of time.time()?

time.time() returns wall-clock time. It can be affected by system clock changes. time.perf_counter() is intended for measuring elapsed time and uses the highest available resolution timer for short durations.

For related timing patterns, see Python timer examples.

Common mistakes

  • Using a busy while True loop in Tkinter. It freezes the GUI. Use after().
  • Using time.time() for elapsed timing. Prefer perf_counter().
  • Forgetting to store elapsed time before stopping. Keep accumulated elapsed time if the stopwatch can pause and resume.
  • Mixing Python 2 and Python 3 Tkinter names. In Python 3, the module name is lowercase: tkinter.
Python Pool infographic showing stopwatch start, lap markers, cumulative times, and split durations
Store successive perf_counter readings to calculate lap and cumulative durations.

Official references

The Python documentation explains time.perf_counter(), time.perf_counter_ns(), and tkinter. The Tk command reference also documents the after() scheduling method used by Tkinter widgets.

Conclusion

Use time.perf_counter() for a Python stopwatch. For terminal scripts, subtract the start reading from the current reading. For a Tkinter stopwatch, schedule display updates with after() so the interface stays responsive.

Use A Monotonic Performance Clock

time.perf_counter() is designed for measuring short elapsed intervals and is not affected by ordinary wall-clock adjustments in the way time.time() can be. Store the start reading and subtract it from the current reading; do not compare formatted strings to calculate duration.

Model Start, Stop, And Reset

When running, keep a start reading and an accumulated duration. Stopping adds the current interval and clears the active start; resuming starts a new interval. Reset clears both accumulated duration and laps. Explicit state prevents double-counting when a button is clicked twice.

Python Pool infographic showing Tkinter window, after callback, elapsed label, and nonblocking event loop
Tkinter's after callback can refresh a display without blocking the event loop with a sleep call.

Record Laps As Durations

A lap can store the elapsed total at the moment it is recorded or the interval since the previous lap. Pick one meaning, document it in the display, and preserve the raw numeric value so formatting changes do not change the measured data.

Format Without Changing The Measurement

Keep elapsed time as a numeric value in seconds and format it only for display. Derive hours, minutes, seconds, and fractional digits with clear integer and remainder operations, then test rollover at 59.999 seconds and 59:59.999. This prevents a rounded label from feeding back into the state or making a paused stopwatch lose precision.

Inject The Clock For Deterministic Tests

Put the clock read behind a small function or object so production code can use perf_counter while tests supply a predictable sequence of readings. A fake clock lets a test advance time without waiting, verify pause boundaries exactly, and prove that a reset discards an old interval. It also keeps the GUI event scheduling separate from the measurement calculation.

Python Pool infographic testing monotonic clock, sleep, resolution, cleanup, and validation
Check monotonic timing, measurement overhead, display refresh, cancellation, and resource cleanup.

Avoid Busy Loops And Drift

A command-line stopwatch can refresh at a modest interval, while a GUI should let its event loop schedule callbacks. The display interval is not the measurement interval: always calculate from clock readings instead of adding a fixed amount for each refresh. This avoids drift when the process is delayed by rendering, other callbacks, or operating-system scheduling.

Keep Tkinter Responsive

Tkinter’s event loop must continue processing redraws and input. Use after() to schedule a short update callback and cancel or ignore stale callbacks when the stopwatch is reset. Do not call time.sleep() in the GUI thread.

Test Timing Without Brittle Exact Values

Inject or wrap the clock when unit testing state transitions, then assert intervals and formatting with tolerances. Test start, repeated start, pause, resume, reset, laps, and window close behavior separately from the real-time display. Include a test for a callback that fires after reset, a second start click while already running, and a lap taken immediately after resume. The state machine should remain correct even when the user clicks faster than the display refresh interval.

The official time.perf_counter reference defines the performance clock. The Tkinter after reference explains scheduled callbacks. Related guidance includes duration conversion and state tests.

For related elapsed-time patterns, compare timer callbacks, benchmark timing, and duration conversion when separating measurement from display.

Frequently Asked Questions

Which Python clock should a stopwatch use?

Use time.perf_counter() for measuring short elapsed intervals because it is designed for high-resolution performance timing and is monotonic.

Why not use time.time() for a stopwatch?

Wall-clock time can be adjusted by synchronization or system changes, so it is not the best source for elapsed-duration measurement.

How do I update a Tkinter stopwatch?

Use the widget’s after() method to schedule the next display update and keep the main event loop responsive instead of calling sleep() in the GUI thread.

How should pause and resume work?

Accumulate completed intervals, store the current start time only while running, and make start, stop, reset, and lap transitions explicit and testable.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted