How to Update a Plot in a Loop in Matplotlib (Python)

To update a Matplotlib plot in a loop, create the plot once, then inside the loop change the data of the existing artist (line.set_ydata() for lines, scatter.set_offsets() for scatter plots) and call plt.pause() so the window redraws. For smooth, repeatable animations use FuncAnimation. This guide shows each way to update a plot in a loop in Python, including scatter plots, real-time data, Jupyter notebooks and the mistakes that make a plot slow or frozen.

Tested with Python 3.12.5, Matplotlib 3.11.2 and NumPy 2.5.3. The screenshots are the real Matplotlib windows at the end of each loop, and the timings come from the Windows Command Prompt. See also the official Matplotlib animations guide and pyplot.pause().

Update a line plot in a loop with plt.pause()

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 4 * np.pi, 200)

plt.ion()                                   # interactive mode: plt.pause() redraws without blocking
fig, ax = plt.subplots(figsize=(7, 4))
line, = ax.plot(x, np.sin(x))               # create the line once
ax.set_ylim(-1.2, 1.2)

for step in range(60):
    line.set_ydata(np.sin(x + step / 5))    # then only change its data
    ax.set_title(f"Frame {step + 1} of 60")
    plt.pause(0.05)                         # draw and let the window process events

plt.ioff()
plt.show()                                  # keep the window open at the end
Matplotlib window showing a sine wave line updated 60 times in a loop with set_ydata and plt.pause, title Frame 60 of 60
The last frame of the loop: the same line object was updated 60 times.
  • plt.ion() turns on interactive mode, so drawing does not block the loop.
  • line.set_ydata() (or set_data(x, y)) changes the data of the line that already exists.
  • plt.pause(interval) redraws the figure and processes window events. Without it the window freezes until the loop ends.
  • plt.ioff() + plt.show() keeps the window open after the last frame.

Why you should not call plot() again in every loop step

Calling ax.plot() inside the loop does not replace the old line; it adds a new one each time, so the plot gets cluttered and slower:

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)
fig, ax = plt.subplots()
for i in range(20):
    ax.plot(x, np.sin(x + i / 3))            # a NEW line every time: nothing is replaced

print("lines on the axes:", len(ax.lines))

Output:

lines on the axes: 20
Command Prompt output showing that calling ax.plot 20 times in a loop leaves 20 lines on the Matplotlib axes
Twenty plot() calls leave twenty lines.

Clearing the axes with ax.clear() (or plt.cla()) before plotting fixes the clutter, but it rebuilds the whole plot, including ticks and labels, every frame. Updating the existing line skips that work:

import time
import matplotlib
matplotlib.use("Agg")                        # draw off screen so only the drawing is timed
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 2000)
frames = 150

fig, ax = plt.subplots()
start = time.perf_counter()
for i in range(frames):
    ax.clear()                               # 1. clear and plot again
    ax.plot(x, np.sin(x + i / 10))
    fig.canvas.draw()
clear_time = time.perf_counter() - start

fig, ax = plt.subplots()
line, = ax.plot(x, np.sin(x))
start = time.perf_counter()
for i in range(frames):
    line.set_ydata(np.sin(x + i / 10))       # 2. update the existing line
    fig.canvas.draw()
update_time = time.perf_counter() - start

print(f"ax.clear() + plot : {clear_time:.2f} s for {frames} frames")
print(f"line.set_ydata()  : {update_time:.2f} s for {frames} frames")
print(f"set_ydata is {clear_time / update_time:.1f}x faster")

Output (one run; the screenshot below is a second run, so the numbers differ):

ax.clear() + plot : 7.13 s for 150 frames
line.set_ydata()  : 5.21 s for 150 frames
set_ydata is 1.4x faster
Command Prompt timing comparison of ax.clear plus plot against line.set_ydata for 150 Matplotlib frames
Updating the data of an existing line compared with clearing and plotting again. The gain grows with the number of ticks, labels and artists on the axes.

Use ax.clear() only when the plot structure really changes (different plot type, different number of lines).

Update a scatter plot in a loop

Scatter plots are PathCollection objects. Move the points with set_offsets() (an N×2 array of x, y) and change their colours with set_array() or sizes with set_sizes():

import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(3)
points = rng.uniform(0, 10, size=(40, 2))
speed = rng.normal(0, 0.25, size=(40, 2))

plt.ion()
fig, ax = plt.subplots(figsize=(6, 5))
sc = ax.scatter(points[:, 0], points[:, 1], c=np.zeros(40), cmap="plasma", vmin=0, vmax=10, s=60)
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
fig.colorbar(sc, label="distance from the centre")

for step in range(80):
    points = (points + speed) % 10                  # move every point
    sc.set_offsets(points)                          # new x, y positions
    sc.set_array(np.hypot(*(points - 5).T) * 1.4)   # new colours
    ax.set_title(f"Step {step + 1}")
    plt.pause(0.03)

plt.ioff()
plt.show()
Matplotlib scatter plot of 40 moving points with plasma colours updated in a loop with set_offsets and set_array
Points moved with set_offsets() and recoloured with set_array().

Set fixed axis limits (or vmin/vmax for colours) before the loop; otherwise the axes and colour scale do not follow the new data.

Real-time plot with changing axis limits

For live data, keep only the last N values in a deque and let Matplotlib recompute the limits with relim() and autoscale_view():

import matplotlib.pyplot as plt
import numpy as np
from collections import deque

rng = np.random.default_rng(7)
readings = deque(maxlen=50)                 # keep only the last 50 values

plt.ion()
fig, ax = plt.subplots(figsize=(7, 4))
line, = ax.plot([], [], color="tab:red")
ax.set_xlabel("Sample")
ax.set_ylabel("Temperature (°C)")

value = 21.0
for i in range(150):
    value += rng.normal(0, 0.3)             # a new "sensor" reading
    readings.append(value)
    line.set_data(range(i - len(readings) + 1, i + 1), readings)
    ax.relim()                              # recompute the data limits ...
    ax.autoscale_view()                     # ... and rescale the axes to them
    plt.pause(0.01)

plt.ioff()
plt.show()
Matplotlib real-time line plot of simulated temperature readings showing the last 50 samples with automatically rescaled axes
A rolling window of the last 50 readings.

In a real program, replace the random value with your sensor, API or file reading.

Animate with FuncAnimation

FuncAnimation calls your update function on a timer, so you do not write the loop yourself. It is the best choice for smooth animations and for saving them as GIF or MP4:

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.animation import FuncAnimation

x = np.linspace(0, 2 * np.pi, 200)
fig, ax = plt.subplots(figsize=(7, 4))
line, = ax.plot(x, np.sin(x), color="tab:green")
ax.set_ylim(-1.2, 1.2)

def update(frame):
    line.set_ydata(np.sin(x + frame / 10))
    return (line,)

ani = FuncAnimation(fig, update, frames=120, interval=30, blit=True)   # keep a reference in a variable!
ani.save("sine_wave.gif", writer="pillow", fps=30)                        # optional: save as GIF
plt.show()
Matplotlib window showing a green sine wave animated with FuncAnimation and blitting
A FuncAnimation of a moving sine wave; the script also saves it as sine_wave.gif.

Always assign the animation to a variable (ani = FuncAnimation(...)). If you do not, Python deletes it and the plot stays still. blit=True redraws only the changed artists; the update function must then return them.

Update a plot in a loop in Jupyter Notebook

In the classic inline backend each figure is a static image, so plt.pause() does nothing useful. Two options work:

from IPython.display import clear_output, display
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 200)
for i in range(30):
    fig, ax = plt.subplots()
    ax.plot(x, np.sin(x + i / 5))
    clear_output(wait=True)      # remove the previous image only when the new one is ready
    display(fig)
    plt.close(fig)

Or install ipympl and run %matplotlib widget; then the set_ydata() + fig.canvas.draw_idle() approach works inside the notebook.

Troubleshooting

ProblemFix
The window is blank or frozen until the loop endsCall plt.pause() in the loop (not time.sleep()) and use plt.ion()
The window closes at the endFinish with plt.ioff() and plt.show()
New data goes outside the plotSet limits first, or call ax.relim() and ax.autoscale_view()
The plot gets slower over timeYou are adding new lines; update the existing ones
FuncAnimation does not moveKeep the animation in a variable and make sure a GUI backend is used

More Matplotlib tutorials you may find useful:

Frequently asked questions

How do I update a Matplotlib plot in a loop?

Create the plot once, keep the line (line, = ax.plot(x, y)), and in the loop call line.set_ydata(new_y) followed by plt.pause(0.05). Turn on interactive mode with plt.ion() first.

How do I update a scatter plot in a loop?

Keep the object returned by ax.scatter() and call sc.set_offsets(xy) with an N×2 array, then plt.pause(). Use set_array() for colours and set_sizes() for sizes.

Why does my plot not update inside a loop?

Usually because the loop never gives Matplotlib time to draw. Use plt.pause() instead of time.sleep(), and turn on interactive mode with plt.ion().

What is the difference between plt.pause and FuncAnimation?

plt.pause() lets you write your own loop and is simple for live data. FuncAnimation runs the updates on a timer, supports blitting and can save the animation to a GIF or video.

How do I refresh a plot in Jupyter Notebook?

Use clear_output(wait=True) and display(fig) in the loop, or switch to the interactive %matplotlib widget backend (ipympl).

How do I update the axis limits when the data changes?

Call ax.relim() and then ax.autoscale_view() after updating the data, or set new limits with ax.set_xlim() and ax.set_ylim().