Here's a little toy program that displays a message like a split-flap display:
#!/usr/bin/python3
import sys
import time
def display(line: str):
cur = '0' * len(line)
while True:
print(cur, end="\r")
if cur == line:
break
time.sleep(0.09)
cur = "".join(chr(min(ord(c) + 1, ord(oc))) for c, oc in zip(cur, line))
print()
message = " ".join(sys.argv[1:])
display(message.upper())
This only works if the script's stdout is unbuffered. Pipe the output through
cat
, and you get a long wait, and then the final string, without the
animation.
What is happening is that since the output is not going to a terminal, optimizations kick in that buffer the output and send it in bigger chunks, to make processing bulk I/O more efficient.
I haven't found a good introductory explanation of buffering in Python's documentation. The details seem to be scattered in the io module documentation and they mostly assume that one is already familiar with concepts like unbuffered, line-buffered or block-buffered. The libc documentation has a good quick introduction that one can read to get up to speed.
Controlling buffering in Python
In Python, one can force a buffer flush with the flush()
method of the output
file descriptor, like sys.stdout.flush()
, to make sure pending buffered
output gets sent.
Python's print()
function also supports flush=True
as an optional argument:
print(cur, end="\r", flush=True)
If one wants to change the default buffering for a file descriptor, since
Python 3.7 there's a convenient reconfigure()
method, which can reconfigure line buffering only:
sys.stdout.reconfigure(line_buffering=True)
Otherwise, the technique is to reassign sys.stdout
to something that has the
behaviour one wants (code from this StackOverflow
thread):
import io
# Python 3, open as binary, then wrap in a TextIOWrapper with write-through.
sys.stdout = io.TextIOWrapper(open(sys.stdout.fileno(), 'wb', 0), write_through=True)
If one needs all this to implement a progressbar, one should make sure to have a look at the progressbar module first.