We are going to build a spinning status indicator that runs while other code is executing.
It will look like this:
Why?
You’ve got some code that takes a while to run.
import time
import random
def slow_func():
seconds = random.randint(2, 5)
time.sleep(seconds)
print("Done!")
if __name__ == '__main__':
slow_func()
Now, when you execute this, you’ll see the following:
You’ll wonder whether your code or your system is working correctly or frozen. Who knows?
Introducing a spinner
Ideally, we’d like to show some sort of activity while our code is executing. We can do that with a spinner. To create a spinner, we can use:
import time
import itertools
def spin():
spinners = ["|", "/", "-", "\\"]
for c in itertools.cycle(spinners):
print(f"\r{c}", end="")
time.sleep(0.1)
- We introduce a list of spinners (
| / - \
). The double backslash is used because of escaping. - Using
itertools.cycle
, we can create an endless cycle of our spinner elements. - In each iteration, we print one of characters.
- By default, Python ends a print statement with a newline. We disable that by printing an empty string (
end=""
) - By putting
\r
in front of our character, we move our cursor back to the start of the line. This is called a carriage return. - We sleep for 100ms.
- By default, Python ends a print statement with a newline. We disable that by printing an empty string (
Combine the spinner with the code
Now, combining them can be done like this:
spin()
slow_func()
But obviously this does not work, since our code executes sequentially.
First the spinner runs to completion, then slow_func
will run.
Due to the endless nature of itertools.cycle
, our code in spin()
never stops.
To solve this, we can run our spinner in its own thread, which allows us to run code in parallel:
import threading
if __name__ == '__main__':
thread = threading.Thread(target=spin)
thread.daemon = True
thread.start()
slow_func()
- We start a new thread, with the
spin
function as its target. - We set
thread.daemon
to True, to make the thread run in the background. - We start the thread.
- We call our slow function
Here’s what it looks like:
Making it awesome
If you want to reuse your code, it wouldn’t be so nice. To fix that, we can introduce a context manager. This will make usage look like this:
with Spinner():
slow_func()
Here’s how we write the context manager:
class Spinner:
def __init__(self):
self.running = False
self.thread = threading.Thread(target=self.spin)
self.thread.daemon = True
def spin(self):
spinners = ["|", "/", "-", "\\"]
for c in itertools.cycle(spinners):
if not self.running:
print("\r", end="")
break
print(f"\r{c}", end="")
time.sleep(0.1)
def __enter__(self):
self.running = True
self.thread.start()
def __exit__(self, exc_type, exc_val, exc_tb):
self.running = False
The logic is:
- We initialize the instance with
Spinner()
. This calls__init__()
, which sets running toFalse
and creates the thread. - After the
with Spinner():
line,Spinner.__enter__()
gets called. We now enter the context and the thread starts running. - Our slow function runs. Meanwhile, every 100ms, a spin character gets printed.
- Our slow function ends and we exit the
with
block. Now,Spinner.__exit__()
gets called. Running will be set toFalse
, which means thespin()
method will break out of its loop, once it detectsself.running
isFalse
.
Further improvements
We can make our code even more dynamic, by allowing you to set the spin timeout and the spinners during class initialization. Here’s the full code:
import itertools
import threading
import time
class Spinner:
def __init__(self, timeout: float = 0.1, spinners: list = ["|", "/", "-", "\\"]):
self.timeout = timeout
self.spinners = spinners
self.running = False
self.thread = threading.Thread(target=self.spin)
self.thread.daemon = True
def spin(self):
for c in itertools.cycle(self.spinners):
if not self.running:
print("\r", end="")
break
print(f"\r{c}", end="")
time.sleep(self.timeout)
def __enter__(self):
self.running = True
self.thread.start()
def __exit__(self, exc_type, exc_val, exc_tb):
self.running = False
You could use any spinners you like, for example:
["⢿", "⣻", "⣽", "⣾", "⣷", "⣯", "⣟", "⡿"]
["👆", "👉", "👇", "👈"]
["| ", " | ", " | ", " |"]
You can find many more examples online, or you can simply make your own.
Final thoughts
I hoped you learned something about how we can indicate activity while you are running your program interactively. If you need a more extensive approach, you can use a library like rich.