How do terminal progress bars work?

2026-03-027:4830code.mendhak.com

How the various kinds of terminal progress indicators and progress bars work behind the scenes and how to create them

Terminal progress indicators are a common sight in command-line applications, often used to show progress of long running tasks and ensuring users don’t get bored. Implementing them in scripts these days is pretty straightforward thanks to various libraries, but I’ve been curious about how they actually work under the hood.

The answer turned out to be very simple; the magic sauce is the character \r, the carriage return character. The carriage return is actually what’s called a control character, it moves the cursor back to the beginning of the line. That in turn allows the next output to overwrite the previous output on the same line. To put it another way, this act of overwriting the previous output is little more than a crude animation technique.

Most modern terminal emulators and environments support this behaviour just fine, and that is how most progress indicators are implemented which I’ll show below. It’ll even work with SSH sessions so you can have progress indicators in remote scripts.

Simple number indicator

Here’s a classic in-place progress number indicator which simply counts to 20. Save it to a Python file and run it.

import

time num_steps

=

20

for

step

in

range

(

num_steps

)

:

print

(

f"Processing {step+1} / {num_steps}"

,

end

=

'\r'

)

time

.

sleep

(

0.3

)

print

(

"\nDone!"

)

Note the use of end='\r' in the print statement in the loop, which is how the in-place update is achieved. Importantly as well, the \n, the newline character on the final print statement is necessary to move the cursor along after the loop is done. Without the newline, the “Done!” message would overwrite the last progress message.

Single character spinner

Single character spinners are a common way to indicate that something is in progress without necessarily showing a percentage. Here, we select from a set of characters in a loop to give the illusion of a spinning animation.

import

time total

=

20

chars

=

[

"|"

,

"/"

,

"-"

,

"\\"

]

for

step

in

range

(

total

)

:

current

=

step

+

1

selected_char

=

chars

[

step

%

len

(

chars

)

]

print

(

f"\r{selected_char} Processing..."

,

end

=

""

)

time

.

sleep

(

0.3

)

print

(

"\nDone!"

)

The key is the use of the modulo operator %, to cycle through the characters in the chars list. Each time the loop iterates, it selects the next character based on the current step, creating a spinning effect.

You can play around with the characters in the chars list to create different styles of spinners. Substitute the chars list as shown here:

chars 

=

[

"⠋"

,

"⠙"

,

"⠹"

,

"⠸"

,

"⠼"

,

"⠴"

,

"⠦"

,

"⠧"

,

"⠇"

,

"⠏"

]

This creates:

See if you can find other interesting characters to use as spinners, here I’ve used the moon phase emojis:

With a ✔ checkmark

You can take it a step further and replace the final progress message with a checkmark to indicate completion, and this is a fairly common pattern and looks nice. The way it works, instead of a newline in the last message, we use another carriage return to overwrite the last progress message.


import

time total

=

20

chars

=

[

"⠋"

,

"⠙"

,

"⠹"

,

"⠸"

,

"⠼"

,

"⠴"

,

"⠦"

,

"⠧"

,

"⠇"

,

"⠏"

]

for

step

in

range

(

total

)

:

current

=

step

+

1

selected_char

=

chars

[

step

%

len

(

chars

)

]

print

(

f"\r{selected_char} Processing..."

,

end

=

""

)

time

.

sleep

(

0.3

)

print

(

f"\r✔ Done! "

)

Here it is:

A progress bar

Now that we understand the basics of in place updates, progress bars aren’t that much more complicated. The idea is to create a string that visually represents the progress using a blocky character that fills up a space.

Try this in a file:

import

time total

=

20

for

step

in

range

(

total

)

:

current

=

step

+

1

percent

=

current

/

total bar_length

=

20

filled

=

int

(

bar_length

*

percent

)

bar

=

"█"

*

filled

+

"-"

*

(

bar_length

-

filled

)

print

(

f"\rProcessing: [{bar}] {current}/{total}"

,

end

=

""

)

time

.

sleep

(

0.1

)

print

(

"\nDone!"

)

The bar string is constructed by repeating the “filled” character for the completed portion and the - character for the remaining portion.

Bouncing dot progress bar

A variation on the progress bar, when you don’t have a known total, is to create a bouncing dot progress bar. In this example, the dot moves forwards or backwards depending on whether its position is less than or greater than the bar length.

import

time bar_length

=

20

for

i

in

range

(

70

)

:

pos

=

i

%

(

bar_length

*

2

)

if

pos

>=

bar_length

:

pos

=

(

bar_length

*

2

)

-

pos

-

1

bar

=

[

"-"

]

*

bar_length bar

[

pos

]

=

"●"

print

(

f"\rProcessing: [{''.join(bar)}]"

,

end

=

""

,

flush

=

True

)

time

.

sleep

(

0.05

)

print

(

"\nDone!"

)

Here it is:

Two progress indicators at once

You might even want to have two progress indicators at once, for example a parent task and nested subtasks.

This does get trickier, as we have to make use of two control sequences, \033[A for “cursor up”, and \033[K for “clear line”.

In this example, we print two lines to reserve space for the progress indicators. Then in each loop, move the cursor up two lines to update the overall progress, then move to the next line to update the loop progress.

import

time

import

sys MOVE_UP

=

"\033[A"

CLEAR_LINE

=

"\033[K"

overall_iterations

=

3

loops

=

10

print

(

"\n\n"

,

end

=

""

)

for

iteration

in

range

(

overall_iterations

)

:

for

loop

in

range

(

loops

)

:

sys

.

stdout

.

write

(

f"{MOVE_UP}{MOVE_UP}"

)

print

(

f"{CLEAR_LINE}Overall Progress ({iteration+1}/{overall_iterations})"

)

print

(

f"{CLEAR_LINE}Processing: [{'#' * (loop+1)}{'-' * (loops-loop-1)}] {loop+1}/{loops}"

)

sys

.

stdout

.

flush

(

)

time

.

sleep

(

0.2

)

print

(

"\nDone!"

)

So to put it another way, we are using the control sequences to move around on the terminal ‘space’ to update relevant lines and make it look like we have two progress indicators at once.

What you should use

The examples here are meant to be educational, or for quick-and-dirty progress indicators without dependencies.

In practice, for production grade scripts, you should consider using a library such as tqdm or rich. They handle a lot of edge cases and have many features and effects that you can easily use.

In Bash

The examples above are all Python for simplicity, but you can do it in Bash too, though it’s a bit more verbose and less readable. Here are the main examples anyway, done in Bash.

The number indicator:

num_steps

=

20

for

((step=1; step<=num_steps; step++))

;

do

printf

"\rProcessing %2d/%2d"

"$step"

"$num_steps"

sleep

0.2

done

echo

-e

"\nDone!"

The single character spinner:

chars

=

(

"|"

,

"/"

"-"

"\\"

)

total

=

20

for

((step=0; step<total; step++))

;

do

char

=

"${chars[step % ${#chars[@]}]}"

printf

"\r%s Processing..."

"$char"

sleep

0.2

done

echo

-e

"\nDone!"

And the progress bar:

total

=

20

bar_size

=

20

for

((i=1; i<=total; i++))

;

do

percent

=

$(( i * 100 / total ))

filled

=

$(( i * bar_size / total ))

empty

=

$(( bar_size - filled ))

bar_str

=

$(printf "%${filled}s" | tr ' ' '#')

empty_str

=

$(printf "%${empty}s" | tr ' ' '-')

echo

-ne

"\rProcessing: [${bar_str}${empty_str}] ${percent}%"

sleep

0.1

done

echo

-e

"\nDone!"


Read the original article

Comments

HackerNews