Don't understand code

Been playing with a very simple count down script. I’ve expanded it with the inputs. I’m happy with the script so far. The problem is the last line. Going to the bottom of the script.

#! python3
# countdown.py - A simple countdown script.

import time
import subprocess

timeLeft = 0
if True:
    try:
        timeLeft = 60 * int(input('How many minutes in the count down? '))
        timeLeft += int(input('And how many seconds? '))
    except ValueError:
        print('You entered a non integer')

while timeLeft >= 0:
    print('Time left is {} minutes, {} seconds.'.format((timeLeft//60),
                                                        timeLeft %60), end='')
    print()
    time.sleep(1)
    timeLeft = timeLeft - 1

# At the end of the countdown, play a sound file.
subprocess.Popen(['start', 'alarm.wav'], shell=True)

This last statement is the problem. I understand all except the [‘start’, ‘alarm.wav’]. Where did they come from? I know what the wave file is, but it doesn’t run the windows sound for alarm. The way it is it has to load a app to sound the alarm. If I understand it correctly code can go in there also. What ever you put in there will run. If true where did the ‘start’ come from?
Thanks

START is used to start programs on Windows. According to its documentation, “Document files can be invoked through their file association just by typing the name of the file as a command.” That means that 'alarm.wav' will run whatever program is associated with a wav file.

2 Likes

You may notice that some times the time will tick down by 2 seconds at a time.

Can you work out why?
Can you fix it to tick down reliably by 1 second at a time?

Can you explain what and where you saw the time not correct for you. I don’t see it on my machine yet. I want to know what the problem is! That’s the fun of programming.
Thank all for helping.

I have written code like your example and had to solve the problem that the time would seem to skip 2 seconds sometimes.

If it is interesting, as part of your learning, you might like to think about why the code will sometimes move on by 2 seconds. If you are just interested in the solution I can provide that. Hint: add print(time.time()) to your loop and notice the time that the print happens each time.

I have identified the problem! That’s the first step. I printed time.time and saw the problem. Some how time needs to return only the whole numbers of time for the sleep part, I think.
More important than the answer is the explaination of the how and why. Running into new problems is a opportunity to learn something new. I might need to know the answer to this sometime in the future.
Thanks for you time.

FYI time.sleep is a fairly coarse tool. You can get some idea of how close-to / far-from “1 second per loop” code like this drifts:

from time import sleep, time

t0 = time()
for i in range(1, 60):
    sleep(1)
    drift = time() - t0 - i
    print(f"{drift:0.3f} seconds late")

Part of the reason for the increasing discrepancy is that the loop and the code in it all themselves take some time to execute, and that is not accounted for in the constant time.sleep. Another part is just that time.sleep is not a high-precision tool. In my experience, obtaining more precise periodic executions is usually done by using an event loop library of some sort, but you could also try manually adjusting the value passed to time.sleep to account for the drift, as @barry-scott suggests, and at least on my system, that keeps each individual loop iteration to within single milliseconds of drift.

Just for the knowledge, how would you address the lag time? How or what is another way to accurately make a count down timer, since time.sleep is so course. I’m not looking for code, rather the concept. Hand some one a fish they eat one meal, teach them where the fish are and how to catch them is doing much more.
Who says learning isn’t fun. Yea, its also hard sometimes.

time.clock_gettime_ns(clk_id) → int
Similar to clock_gettime() but return time as nanoseconds.

Availability: Unix.

New in version 3.7.

Is this where I may have to go and just do the math.

By Leonard Dye via Discussions on Python.org at 29Aug2022 22:06:

Just for the knowledge, how would you address the lag time? How or what is another way to accurately make a count down timer, since time.sleep is so course. I’m not looking for code, rather the concept. Hand some one a fish they eat one meal, teach them where the fish are and how to catch them is doing much more.
Who says learning isn’t fun. Yea, its also hard sometimes.

time.sleep() is usually fairly accurate (by my stogy old standards -
when I grew up it had 1 second resolution, none of this subsecond
precision!)

However, there’s variation introduced by (a) how long the other
operations in the loop take and (b) the OS scheduler giving time back to
Python after the sleep, and potentially similar effects from Python’s
scheduler if you’re using threads.

If “on time” precision is wanted, the typical approach is:

  • compute the “next_time” at the top of the loop, either as now+delay
    or as an even more rigid "now_before_the_loop+loop_counts*delay`
  • do the loop stuff
  • compute the remaining sleep time as next_time-now
  • sleep that long if that is >0

Cheers,
Cameron Simpson cs@cskk.id.au

Thanks for the picture of the process for making an accurate count down timer. I didn’t set out to learn about time problems, but that’s great. The picture of the problem, the possible solutions may help in another project where accuracy is needed. This is where timeit comes in handy, understanding how long the processed code takes.
I can see that exploring is part of the process one goes through when planning a software project, as well as details about the project. This was not set out when I started many projects before. This is something I will work on.

Another way to look at the drift:

from time import sleep, time
from statistics import mean
drifts = []
for i in range(60):
    t0 = time()
    sleep(1)
    drifts.append(time() - t0 - 1)

That gives you a list of drifts that you can perform additional statistics on. When I try it, I get:

  • sum(drifts), the total drift over one minute, is 0.07 seconds;
  • max(drifts) was 1.3ms;
  • min(drifts) was 0.5ms;
  • mean(drifts) (the average) was 1.2ms.

But of course time.sleep() cannot make any guarantees about the longest it will sleep. If you run sleep(10) on your laptop and then immediately put the laptop into hibernation mode for a day, sleep will not wake until one day and ten seconds later :slight_smile:

Here is the project so far. I’m happy with it, except, The output that prints in on a different line each second. Should print in the same place on the terminal. So has to clr screen and then print again in the old days. I thought the end=‘’ was to do that.

import time
import subprocess

timeLeft = 0
if True:
    try:
        timeLeft+= 3600 * int(input(' How many hours in the count down? '))
        timeLeft += 60 * int(input('How many minutes in the count down? '))
        timeLeft += int(input('And how many seconds? '))
    except ValueError:
        print('You entered a non integer')

while timeLeft >= 0:
    print('Time left is {} hours {} minutes, {} seconds.'
          .format(timeLeft//3600, timeLeft//60 %60, timeLeft %60), end='')
    print()
    time.sleep(1)
    timeLeft -= 1

# At the end of the countdown, play a sound file. Must have sound system on.
subprocess.Popen(['start', 'alarm.wav'], shell=True)

The end parameter specifies what to put at the end of the printed text. By default it is a newline character, meaning subsequent prints happen on new lines. By setting to the empty string, that keeps all the output on one line, but doesn’t do any sort of backtracking or “deletion” over previous text, it just prints in the next spot.

That sort of fancy console UI handling (deletions, overwrites, etc) is platform-dependent and somewhat involved, which is why there are entire libraries, e.g. Rich, devoted to just offering these features .

I found one way. Its brute force. print(‘\n’*20). Still not good enough.

while timeLeft >= 0:
    print('\n' * 20)
    print('Time left is {} hours {} minutes, {} seconds.'
          .format(timeLeft//3600, timeLeft//60 %60, timeLeft %60), end='')
    # print('\n' * 10)
    time.sleep(1)
    timeLeft -= 1

You print with end='' and then immediately, on the very next line, print a blank line.

print('...', end='')
print()

So the second print puts in the newline that was suppressed in the first print.

To print over the same line again and again, you need a couple of tricks:

  • carriage return makes the print location go back to the start of the line;
  • you need to suppress the newline;
  • you need to pad the string with extra spaces, or else short lines will not cover up previous long lines;
  • you need to flush the print buffer, or else nothing will print until the end of the loop;
  • and finally you need one extra regular print at the end.

Putting that together, try adapting this recipe:

from time import sleep
try:
    for  i in range(10000):
        print('\rCounter: %-6d' % i, flush=True, end='')
        sleep(0.05)
finally:
    print()
1 Like

This is interesting! Please correct me if wrong, only had this for about 30 minutes. The main print is of interest: carriage return holds Counter: in place, interger precision, flushing data in Counter, suppressed the new line.
Maybe by playing with the sleep setting we could be able to get close to one tic per second. There is a lot to still be learned and thought about.

By Leonard Dye via Discussions on Python.org at 31Aug2022 02:06:

This is interesting! Please correct me if wrong, only had this for
about 30 minutes. The main print is of interest: carriage return holds
Counter: in place, interger precision, flushing data in Counter,
suppressed the new line.

I think you’re overreading this:

print('\rCounter: %-6d' % i, flush=True, end='')

This print outputs something like '\rCount: 13' to your
terminal. The effect of that string is:

  • \r: move the terminal cursor to the beginning of the current line
  • the rest of the text overwrites what was displayed before

Because the " 13" part is fixed width, this always overwirte what was
there before; if the string changed length you’d need to do a little
more if there was trailing text to erase.

The parameters to the print() call have the following effects to the
text which print() writes to the terminal:

  • flush=True: this causes the text to appear on the terminal
    immediately; default behaviour for the standard output is line
    buffering, which only updates the terminal when a newline character is
    encountered
  • end='': this replaces the default line ending used by print(),
    which is a newline character '\n', with an empty string;
    this leaves the terminal cursor at the end of the line instead of
    advancing to the following line

The flush=True becomes necessary because you’ve suppressed that
newline character, which otherwise would have caused the output to
appear immediately.

All print() does is write output to your terminal.

Cheers,
Cameron Simpson cs@cskk.id.au

Got it done! The output print on the same line now. No more printing on new line each time. Used the new f string formating instead of .format.

import time
import subprocess
import os

timeLeft = 0
if True:
    try:
        timeLeft+= 3600 * int(input(' How many hours in the count down? '))
        timeLeft += 60 * int(input('How many minutes in the count down? '))
        timeLeft += int(input('And how many seconds? '))
    except ValueError:
        print('You entered a non integer')


while timeLeft >= 0:
    print(f'\rTime left is {timeLeft//3600} hours {timeLeft//60 %60} minutes'
          f' {timeLeft %60} seconds', flush=True, end='')
    time.sleep(1)
    timeLeft -= 1

# At the end of the countdown, play a sound file. Must have sound system and
# speakers on.
subprocess.Popen(['start', 'alarm.wav'], shell=True)

Thanks to all who helped with this project. I learned a lot, hope it helped others.

Thanks to this thread, I learned how to use \r. I thought CR was a return code only for mac users. Instead of the following code:

N = 10000
for i in range(N):
    print('\b'*80 + 'Counter: {:-5d}'.format(N-i), end='', flush=1)
    ...

I will use:

for i in range(N):
    print('\rCounter: {:-5d}'.format(N-i), end='')
    ...

Using \r is faster than \b and there are no annoying characters ^H^H^H^H^H^H^H... if the output is piped to a file.

I’d like to add a few points here.

If the string contains \r, it seems to immediately flush as well as \n, at least on Windows 10 where I tested with.

If something printed stdout or stderr while counting in the loop, the message will start after the Counter: {:-5d} message. If it is not preferable, I think the following code is better:

for i in range(N):
    print('Counter: {:-5d}'.format(N-i), end='\r')
    ...

Windows uses CR/LF
Unix uses LF
Mac OS classic (motorola M68000 era macs) used CR
macOS uses LF