Introduction
Creating a countdown timer in Python can be a fun and rewarding project, especially when you extend its capabilities beyond a basic terminal application. In this guide, we will walk through the process of building a robust countdown timer that includes a graphical user interface (GUI), support for multiple timers, desktop notifications, and sound alerts. By the end of this tutorial, you will have a comprehensive understanding of how to create a feature-rich countdown timer in Python that can be used in various scenarios, from managing study sessions to timing workouts.
Step 1: Setting Up the Basic Countdown Timer
We begin by creating a simple countdown timer that works in the terminal. This timer will take the number of seconds as input and will count down to zero, displaying the time in a “HH:MM” format.
import time
def countdown_timer(seconds):
while seconds:
hours, remainder = divmod(seconds, 3600)
mins, secs = divmod(remainder, 60)
time_format = '{:02d}:{:02d}:{:02d}'.format(hours, mins, secs)
print(time_format, end='\r')
time.sleep(1)
seconds -= 1
print("Time's up!")
Explanation:
- We use
divmod
to break down the total seconds into hours, minutes, and seconds. - The
print
statement withend='\r'
allows us to overwrite the previous output, creating an updating display.
Step 2: Enhancing the Timer with Pause/Resume and Sound Notifications
Next, we introduce the ability to pause and resume the timer, as well as play a sound when the timer finishes. We also include logging to keep track of the timer’s activity.
import time
import threading
import os
from datetime import datetime
class CountdownTimer:
def __init__(self, name, seconds, end_message="Time's up!", sound=False):
self.name = name
self.total_seconds = seconds
self.seconds = seconds
self.end_message = end_message
self.sound = sound
self.running = False
self.paused = False
def start(self):
self.running = True
self.log(f"Timer '{self.name}' started.")
self.run()
def run(self):
while self.seconds and self.running:
if not self.paused:
hours, remainder = divmod(self.seconds, 3600)
mins, secs = divmod(remainder, 60)
time_format = '{:02d}:{:02d}:{:02d}'.format(hours, mins, secs)
print(f"{self.name}: {time_format}", end='\r')
time.sleep(1)
self.seconds -= 1
else:
time.sleep(1)
if self.running and not self.paused:
self.finish()
def pause(self):
self.paused = True
self.log(f"Timer '{self.name}' paused.")
def resume(self):
self.paused = False
self.log(f"Timer '{self.name}' resumed.")
self.run()
def stop(self):
self.running = False
self.log(f"Timer '{self.name}' stopped.")
print(f"\nTimer '{self.name}' stopped.")
def finish(self):
print(f"\n{self.name}: {self.end_message}")
self.log(f"Timer '{self.name}' finished.")
if self.sound:
self.play_sound()
def play_sound(self):
if os.name == 'posix': # macOS, Linux
os.system('afplay /System/Library/Sounds/Glass.aiff')
elif os.name == 'nt': # Windows
import winsound
winsound.Beep(1000, 500)
def log(self, message):
with open("timer_log.txt", "a") as log_file:
log_file.write(f"{datetime.now()}: {message}\n")
Explanation:
- Pause/Resume: The
pause
andresume
methods allow the timer to stop and start without resetting the countdown. - Sound Notification: The
play_sound
method plays a sound when the timer finishes, using platform-specific commands. - Logging: The
log
method writes events (start, pause, resume, stop, finish) to atimer_log.txt
file for record-keeping.
Step 3: Adding a Graphical User Interface (GUI) with Tkinter
To make our timer more user-friendly, we can add a GUI using Tkinter, Python’s standard GUI library. The GUI will allow users to input the timer details (name, duration, and end message) and start the timer with a click of a button.
import tkinter as tk
from tkinter import messagebox
class TimerApp:
def __init__(self, root):
self.root = root
self.root.title("Countdown Timer")
self.root.geometry("400x300")
self.root.configure(bg='#2c2c2c') # Dark mode
self.timers = []
self.create_widgets()
def create_widgets(self):
tk.Label(self.root, text="Timer Name:", bg='#2c2c2c', fg='white').pack(pady=10)
self.name_entry = tk.Entry(self.root)
self.name_entry.pack()
tk.Label(self.root, text="Time (seconds):", bg='#2c2c2c', fg='white').pack(pady=10)
self.time_entry = tk.Entry(self.root)
self.time_entry.pack()
tk.Label(self.root, text="End Message:", bg='#2c2c2c', fg='white').pack(pady=10)
self.message_entry = tk.Entry(self.root)
self.message_entry.pack()
self.sound_var = tk.BooleanVar()
tk.Checkbutton(self.root, text="Sound", variable=self.sound_var, bg='#2c2c2c', fg='white').pack(pady=5)
tk.Button(self.root, text="Start Timer", command=self.start_timer).pack(pady=10)
tk.Button(self.root, text="Quit", command=self.root.quit).pack(pady=10)
def start_timer(self):
try:
name = self.name_entry.get()
seconds = int(self.time_entry.get())
message = self.message_entry.get() or "Time's up!"
sound = self.sound_var.get()
timer = CountdownTimer(name, seconds, message, sound)
self.timers.append(timer)
timer_thread = threading.Thread(target=timer.start)
timer_thread.start()
messagebox.showinfo("Timer Started", f"Timer '{name}' started for {seconds} seconds.")
except ValueError:
messagebox.showerror("Invalid Input", "Please enter a valid number for the time in seconds.")
Explanation:
- Tkinter Widgets: We use labels, entry fields, and buttons to create an interactive interface for the user.
- Dark Mode: The GUI is styled with a dark background for a modern look.
- Event Handling: The
start_timer
method is triggered when the user clicks the “Start Timer” button, launching a new timer in a separate thread.
Step 4: Implementing Desktop Notifications and Command-Line Arguments
To make our timer even more versatile, we add desktop notifications using the plyer
library and allow the timer to be configured via command-line arguments using the argparse
library.
from plyer import notification
import argparse
def parse_arguments():
parser = argparse.ArgumentParser(description="Command-line Countdown Timer")
parser.add_argument("--name", type=str, default="Timer", help="Name of the timer")
parser.add_argument("--time", type=int, required=True, help="Time in seconds")
parser.add_argument("--message", type=str, default="Time's up!", help="End message")
parser.add_argument("--sound", action="store_true", help="Play sound when timer ends")
return parser.parse_args()
def main():
args = parse_arguments()
if args.time:
timer = CountdownTimer(args.name, args.time, args.message, args.sound)
timer_thread = threading.Thread(target=timer.start)
timer_thread.start()
root = tk.Tk()
app = TimerApp(root)
root.mainloop()
if __name__ == "__main__":
main()
Explanation:
- Command-Line Arguments: Users can now start a timer from the terminal by specifying the name, duration, end message, and whether a sound should be played.
- Desktop Notifications: The
plyer
library is used to send a desktop notification when the timer ends, ensuring the user is alerted even if the GUI is minimized.
Final Product: Full Python Script
Here’s the complete script with all the features we’ve discussed:
import time
import threading
import os
from datetime import datetime
import tkinter as tk
from tkinter import messagebox
from plyer import notification
import argparse
class CountdownTimer:
def __init__(self, name, seconds, end_message="Time's up!", sound=False):
self.name = name
self.total_seconds = seconds
self.seconds = seconds
self.end_message = end_message
self.sound = sound
self.running = False
self.paused = False
def start(self):
self.running = True
self.log(f"Timer '{self.name}' started.")
self.run()
def run(self):
while self.seconds and self.running:
if not self.paused:
hours, remainder = divmod(self.seconds, 3600)
mins, secs = divmod(remainder, 60)
time_format = '{:02d}:{:02d}:{:02d}'.format(hours, mins, secs)
print(f"{self.name}: {time_format}", end='\r')
time.sleep(1)
self.seconds -= 1
else:
time.sleep(1)
if self.running and not self.paused:
self.finish()
def pause(self):
self.paused = True
self.log(f"Timer '{self.name}' paused.")
def resume(self):
self.paused = False
self.log(f"Timer '{self.name}' resumed.")
self.run()
def stop(self):
self.running = False
self.log(f"Timer '{self.name}' stopped.")
print(f"\nTimer '{self.name}' stopped.")
def finish(self):
print(f"\n{self.name}: {self.end_message}")
self.log(f"Timer '{self.name}' finished.")
if self.sound:
self.play_sound()
notification.notify(
title=f"Timer '{self.name}' Finished",
message=self.end_message,
timeout=5
)
def play_sound(self):
if os.name == 'posix': # macOS, Linux
os.system('afplay /System/Library/Sounds/Glass.aiff')
elif os.name == 'nt': # Windows
import winsound
winsound.Beep(1000, 500)
def log(self, message):
with open("timer_log.txt", "a") as log_file:
log_file.write(f"{datetime.now()}: {message}\n")
class TimerApp:
def __init__(self, root):
self.root = root
self.root.title("Countdown Timer")
self.root.geometry("400x300")
self.root.configure(bg='#2c2c2c') # Dark mode
self.timers = []
self.create_widgets()
def create_widgets(self):
tk.Label(self.root, text="Timer Name:", bg='#2c2c2c', fg='white').pack(pady=10)
self.name_entry = tk.Entry(self.root)
self.name_entry.pack()
tk.Label(self.root, text="Time (seconds):", bg='#2c2c2c', fg='white').pack(pady=10)
self.time_entry = tk.Entry(self.root)
self.time_entry.pack()
tk.Label(self.root, text="End Message:", bg='#2c2c2c', fg='white').pack(pady=10)
self.message_entry = tk.Entry(self.root)
self.message_entry.pack()
self.sound_var = tk.BooleanVar()
tk.Checkbutton(self.root, text="Sound", variable=self.sound_var, bg='#2c2c2c', fg='white').pack(pady=5)
tk.Button(self.root, text="Start Timer", command=self.start_timer).pack(pady=10)
tk.Button(self.root, text="Quit", command=self.root.quit).pack(pady=10)
def start_timer(self):
try:
name = self.name_entry.get()
seconds = int(self.time_entry.get())
message = self.message_entry.get() or "Time's up!"
sound = self.sound_var.get()
timer = CountdownTimer(name, seconds, message, sound)
self.timers.append(timer)
timer_thread = threading.Thread(target=timer.start)
timer_thread.start()
messagebox.showinfo("Timer Started", f"Timer '{name}' started for {seconds} seconds.")
except ValueError:
messagebox.showerror("Invalid Input", "Please enter a valid number for the time in seconds.")
def parse_arguments():
parser = argparse.ArgumentParser(description="Command-line Countdown Timer")
parser.add_argument("--name", type=str, default="Timer", help="Name of the timer")
parser.add_argument("--time", type=int, required=True, help="Time in seconds")
parser.add_argument("--message", type=str, default="Time's up!", help="End message")
parser.add_argument("--sound", action="store_true", help="Play sound when timer ends")
return parser.parse_args()
def main():
args = parse_arguments()
if args.time:
timer = CountdownTimer(args.name, args.time, args.message, args.sound)
timer_thread = threading.Thread(target=timer.start)
timer_thread.start()
root = tk.Tk()
app = TimerApp(root)
root.mainloop()
if __name__ == "__main__":
main()
Conclusion
Building a countdown timer in Python is an excellent way to explore different programming concepts, such as multi-threading, GUI development, and user interaction. By following the steps in this guide, you’ve created a versatile timer that can be used in various situations, from time management to productivity enhancement. You now have a powerful tool that can be expanded further or integrated into larger projects. Happy coding!