using datetime module to create pause-able timer getting weird results

Question:

I’m trying to create a pause-able timer in Python.
During my search I came across this answer.
https://stackoverflow.com/a/60027719/11892518

I modified the code a little and added the timer in a while loop I also used the keyboard module for human interaction while the loop is running:

from datetime import datetime, timedelta
import time
import keyboard


class MyTimer:
    """
    timer.start() - should start the timer
    timer.pause() - should pause the timer
    timer.resume() - should resume the timer
    timer.get() - should return the current time
    """
    
    def __init__(self):
        print("Initializing timer")
        self.time_started : datetime = None
        self.time_paused : datetime = None
        self.is_paused : bool = False
    
    def Start(self):
        """ Starts an internal timer by recording the current time """
        #print("Starting timer")
        self.time_started = datetime.now()
    
    def Pause(self):
        """ Pauses the timer """
        if self.time_started is None:
            pass
            #raise ValueError("Timer not started")
        if self.is_paused:
            pass
            #raise ValueError("Timer is already paused")
        #print("Pausing timer")
        self.time_paused = datetime.now()
        self.is_paused = True
        
    def Resume(self):
        """ Resumes the timer by adding the pause time to the start time """
        if self.time_started is None:
            pass
            #raise ValueError("Timer not started")
        if not self.is_paused:
            pass
            #raise ValueError("Timer is not paused")
        #print("Resuming timer")
        dt : timedelta = datetime.now() - self.time_paused
        #print(dt)
        #print(self.time_started)
        #print(self.time_started + dt)
        self.time_started = self.time_started + dt
        
        self.is_paused = False
    
    def Get(self):
        """ Returns a timedelta object showing the amount of time elapsed since the start time, less any pauses """
        #print("Get timer value")
        if self.time_started is None:
            pass
            #raise ValueError("Timer not started")
        now : timedelta = datetime.now() - self.time_started
        if self.is_paused:
            return self.time_paused - self.time_started
        else:
            return now
        

def timer_loop():
    t1 = MyTimer()
    
    while True:
        try:
            if keyboard.is_pressed('s'):
                t1.Start()
                
            elif keyboard.is_pressed('p'):
                t1.Pause()
                
            elif keyboard.is_pressed('r'):
                t1.Resume()
                
            elif keyboard.is_pressed('g'):
                print(t1.Get())
            elif keyboard.is_pressed('q'):
                print("Terminating...")
                break
                
        except Exception as ex:
            print(ex)
            break




if __name__ == '__main__':
    timer_loop()

The problem appears after the timer has been started, paused, resumed and paused again.

The result is like this:

-1 day, 21:12:31.704640
-1 day, 21:12:31.704640
-1 day, 21:12:31.704640

Note: when the loop is running press s to start the timer, p to pause, g to get value, r to resume the timer, and q to break the loop and exit the program.

Could anyone please explain why this happens? I added type hinting because I saw on the internet that it could be caused by conversion between time and float, but still that didn’t resolve it.


SOLUTION:
This is my final code in case anyone stumbles upon this question:

from datetime import datetime, timedelta
import time
import keyboard


class MyTimer:
    """
    timer.start() - should start the timer
    timer.pause() - should pause the timer
    timer.resume() - should resume the timer
    timer.get() - should return the current time
    """
    
    def __init__(self):
        print("Initializing timer")
        self.time_started : datetime = None
        self.time_paused : datetime = None
        self.is_paused : bool = False
    
    def Start(self):
        """ Starts an internal timer by recording the current time """
        print("Starting timer")
        self.time_started = datetime.now()
    
    def Pause(self):
        """ Pauses the timer """
        if self.time_started is None:
            #raise ValueError("Timer not started")
            return
        if self.is_paused:
            #raise ValueError("Timer is already paused")
            return
        print("Pausing timer")
        self.time_paused = datetime.now()
        self.is_paused = True
        
    def Resume(self):
        """ Resumes the timer by adding the pause time to the start time """
        if self.time_started is None:
            #raise ValueError("Timer not started")
            return
        if not self.is_paused:
            #raise ValueError("Timer is not paused")
            return
        print("Resuming timer")
        dt : timedelta = datetime.now() - self.time_paused
        
        self.time_started = self.time_started + dt
        
        self.is_paused = False
    
    def Get(self):
        """ Returns a timedelta object showing the amount of time elapsed since the start time, less any pauses """
        print("Get timer value")
        if self.time_started is None:
            #raise ValueError("Timer not started")
            return
        
        if self.is_paused:
            paused_at : timedelta = self.time_paused - self.time_started
            print(paused_at)
            return paused_at
            
        else:
            now : timedelta = datetime.now() - self.time_started
            print(now)
            return now
        

def timer_loop():
    t1 = MyTimer()
    
    keyboard.add_hotkey('s', t1.Start)
    keyboard.add_hotkey('p', t1.Pause)
    keyboard.add_hotkey('r', t1.Resume)
    keyboard.add_hotkey('g', t1.Get)
    
    try:
        keyboard.wait('q')
    except Exception as ex:
        print(ex)
    


if __name__ == '__main__':
    timer_loop()
    
Asked By: zooid

||

Answers:

The way your keyboard loop works is that it calls the method multiple times while the key is pressed (evident from the multiple prints when you press g).

When the timer is resumed, the pause time is subtracted. In your version, the block is disabled for only continuing if the timer is actually paused, so the pause time gets subtracted multiple times, ultimately resulting in a negative timer display if that happens several times.

One way to fix that is to replace pass with return if the timer is not paused.

Additionally, a pattern described in the keyboard docs is to remove the loop and add callbacks. This ensures that the method is only called once per keypress.

def timer_loop():
    t1 = MyTimer()
    
    keyboard.add_hotkey('s', t1.Start)
    keyboard.add_hotkey('p', t1.Pause)
    keyboard.add_hotkey('r', t1.Resume)
    keyboard.add_hotkey('g', lambda: print(t1.Get()))
    
    try:
        keyboard.wait('q')
    except Exception as ex:
        print(ex)

However, this still leaves the possibility of a negative output if r is pressed multiple times in succession. You can see this if you press the sequence s (wait a few seconds) g p g g (to confirm it’s paused) then [r g] a few times – you’ll see the timer go down and then negative as the pause time is repeatedly subtracted.

Two additional points to call out:

  • Getting the timer state before it’s started raises an exception because self.time_started is subtracted from datetime.now() before self.time_started is initialized.
  • When the timer is paused when it’s already paused, the earlier pause is overwritten and lost.

Reinstating all the ValueError lines, or at least preventing code continuation within those methods, would avoid all the above issues including your original issue.

Answered By: scign
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.