I just found the solution that I'm going to use. The solution has plenty of examples here.
I've modified the example in the original post with both the solution as well as a time delay for visualization purposes (the final output is the same with or without the delay).
from time import sleep
from tqdm import tqdm_notebook
for i in tqdm_notebook(range(3)):
for j in tqdm_notebook(range(5)):
sleep(0.1)
print(i," : ", j)
print("Done!")
The final output looks like this. While it's processing, it's pleasant to watch at (no jumping around or anything crazy).

One little hack that I'm now doing to make this a super easy drop-in replacement is to pull in tqdm like this, so I don't have to change any other code:
from time import sleep
from tqdm import tqdm_notebook as tqdm
for i in tqdm(range(3)):
for j in tqdm(range(5)):
sleep(0.1)
print(i," : ", j)
print("Done!")