matplotlib
non-blocking
data visualization
python plotting
interactive graphs

Plotting in a non-blocking way with Matplotlib

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

To plot with Matplotlib without blocking your script, enable interactive mode with plt.ion() and use plt.pause() to process GUI events. This lets the plot window stay open and update while your code continues executing. For a one-shot non-blocking display, plt.show(block=False) also works, but you still need plt.pause() to keep the window responsive.

The Blocking Problem

By default, plt.show() blocks program execution until you close the plot window. This is fine for one-off analysis, but it stops your script dead when you need to update a plot in real time or continue processing data while the plot is visible.

python
1import matplotlib.pyplot as plt
2
3plt.plot([1, 2, 3], [1, 4, 9])
4plt.show()  # Script stops here until you close the window
5print("This only prints after the window closes")

Method 1: Interactive Mode with plt.ion()

Interactive mode is the recommended approach for real-time plotting. It tells Matplotlib to draw without blocking and to accept updates to existing figures:

python
1import matplotlib.pyplot as plt
2import numpy as np
3import time
4
5plt.ion()  # Enable interactive mode
6
7fig, ax = plt.subplots()
8line, = ax.plot([], [], "b-o")
9ax.set_xlim(0, 50)
10ax.set_ylim(-1.5, 1.5)
11ax.set_title("Live Sine Wave")
12
13x_data = []
14y_data = []
15
16for i in range(50):
17    x_data.append(i)
18    y_data.append(np.sin(i * 0.2))
19
20    line.set_data(x_data, y_data)
21    fig.canvas.draw_idle()
22    plt.pause(0.05)  # Process GUI events and pause briefly
23
24plt.ioff()  # Disable interactive mode
25plt.show()  # Final blocking show to keep window open

The key calls:

  • plt.ion() enables interactive mode
  • fig.canvas.draw_idle() schedules a redraw
  • plt.pause(0.05) processes GUI events and gives the backend time to repaint
  • plt.ioff() and plt.show() at the end keep the final plot visible

Method 2: show(block=False)

For simpler cases where you just want the plot to appear without halting the script:

python
1import matplotlib.pyplot as plt
2
3fig, ax = plt.subplots()
4ax.plot([1, 2, 3, 4, 5], [1, 4, 9, 16, 25])
5ax.set_title("Non-blocking plot")
6
7plt.show(block=False)
8print("Script continues immediately")
9
10# Do other work here
11result = sum(range(1000000))
12print(f"Computation result: {result}")
13
14# Keep the window alive for a few seconds
15plt.pause(5)

Without the plt.pause() at the end, the window may flash and close immediately when the script finishes.

Updating Plot Data Efficiently

A common mistake is calling ax.plot() repeatedly inside a loop. This creates a new Line2D object every iteration, accumulating objects and slowing down the figure. Instead, create the artist once and update its data:

python
1import matplotlib.pyplot as plt
2import numpy as np
3
4plt.ion()
5fig, ax = plt.subplots()
6
7# Create artists once
8line, = ax.plot([], [], "r-", linewidth=2)
9scatter = ax.scatter([], [], c="blue", s=50)
10ax.set_xlim(0, 100)
11ax.set_ylim(0, 100)
12
13for i in range(100):
14    x = np.arange(i + 1)
15    y = np.sqrt(x) * 10
16
17    # Update existing artists
18    line.set_data(x, y)
19    scatter.set_offsets(np.column_stack([x, y]))
20
21    ax.set_xlim(0, max(10, i + 1))
22    fig.canvas.draw_idle()
23    plt.pause(0.02)
24
25plt.ioff()
26plt.show()

Performance Comparison

ApproachMemorySpeedRecommended
ax.plot() in every iterationGrows linearlyDegrades over timeNo
line.set_data() to updateConstantConsistentYes
ax.clear() then ax.plot()ConstantSlower (full redraw)Only if axes change
canvas.blit() (blitting)ConstantFastestFor high frame rates

Blitting for High Performance

When you need higher frame rates (monitoring dashboards, animations), blitting redraws only the changed parts of the canvas:

python
1import matplotlib.pyplot as plt
2import numpy as np
3
4fig, ax = plt.subplots()
5ax.set_xlim(0, 2 * np.pi)
6ax.set_ylim(-1.2, 1.2)
7ax.set_title("High-performance animation with blitting")
8
9x = np.linspace(0, 2 * np.pi, 200)
10line, = ax.plot(x, np.sin(x), "g-", animated=True)
11
12fig.canvas.draw()
13background = fig.canvas.copy_from_bbox(ax.bbox)
14
15plt.show(block=False)
16
17for phase in np.linspace(0, 10 * np.pi, 500):
18    fig.canvas.restore_region(background)
19    line.set_ydata(np.sin(x + phase))
20    ax.draw_artist(line)
21    fig.canvas.blit(ax.bbox)
22    fig.canvas.flush_events()
23
24plt.show()

Blitting avoids redrawing the axes, labels, and background on every frame. On supported backends, this can be 5 to 10 times faster than full redraws.

Using FuncAnimation for Structured Animation

For production animation code, FuncAnimation provides a cleaner structure than manual loops:

python
1import matplotlib.pyplot as plt
2import matplotlib.animation as animation
3import numpy as np
4
5fig, ax = plt.subplots()
6x = np.linspace(0, 2 * np.pi, 200)
7line, = ax.plot(x, np.sin(x))
8ax.set_ylim(-1.2, 1.2)
9
10def update(frame):
11    line.set_ydata(np.sin(x + frame * 0.1))
12    return (line,)
13
14ani = animation.FuncAnimation(fig, update, frames=200, interval=30, blit=True)
15plt.show()

FuncAnimation handles the event loop, timing, and blitting automatically. You can also save the animation to a file:

python
ani.save("animation.gif", writer="pillow", fps=30)

Backend Configuration

Non-blocking plotting requires an interactive GUI backend. Common backends:

BackendPlatformSet With
TkAggAll platforms (default)matplotlib.use("TkAgg")
Qt5AggAll platforms (needs PyQt5)matplotlib.use("Qt5Agg")
macosxmacOS nativematplotlib.use("macosx")
AggHeadless (no GUI)matplotlib.use("Agg")
inlineJupyter notebooks%matplotlib inline
widgetJupyter (interactive)%matplotlib widget

The Agg backend is non-interactive and cannot display windows. If your script silently does nothing when you call plt.show(), check your backend:

python
1import matplotlib
2print(matplotlib.get_backend())
3
4# Switch backend (must be done before importing pyplot)
5import matplotlib
6matplotlib.use("TkAgg")
7import matplotlib.pyplot as plt

Jupyter Notebooks

Non-blocking behavior in Jupyter differs from desktop scripts:

python
1# For static inline plots (default in most notebooks)
2%matplotlib inline
3
4# For interactive plots that update in the notebook
5%matplotlib widget
6# Requires: pip install ipympl

With %matplotlib widget, you get a live, interactive plot directly in the notebook cell that you can update programmatically.

Threading Considerations

Matplotlib is not thread-safe. If you update plots from a background thread while the main thread runs the GUI event loop, you will get crashes or corrupted renders. Keep all Matplotlib calls on the main thread:

python
1import matplotlib.pyplot as plt
2import threading
3import queue
4import time
5
6data_queue = queue.Queue()
7
8def data_producer():
9    """Runs in a background thread, produces data."""
10    for i in range(100):
11        data_queue.put(i ** 0.5)
12        time.sleep(0.1)
13
14# Start data producer in a background thread
15thread = threading.Thread(target=data_producer, daemon=True)
16thread.start()
17
18# All Matplotlib calls stay on the main thread
19plt.ion()
20fig, ax = plt.subplots()
21line, = ax.plot([], [])
22y_data = []
23
24while thread.is_alive() or not data_queue.empty():
25    while not data_queue.empty():
26        y_data.append(data_queue.get())
27    line.set_data(range(len(y_data)), y_data)
28    ax.relim()
29    ax.autoscale_view()
30    fig.canvas.draw_idle()
31    plt.pause(0.1)
32
33plt.ioff()
34plt.show()

Common Pitfalls

Forgetting plt.pause() after show(block=False). Without pause, the GUI event loop never processes events, and the window appears frozen or closes immediately when the script exits.

Calling ax.plot() inside a loop instead of updating artists. Each plot() call creates a new Line2D object. After thousands of iterations, the figure becomes unresponsive. Use line.set_data() or line.set_ydata() instead.

Running on a headless server with no display. Non-blocking interactive plots require a display server (X11, Wayland, or macOS Aqua). On servers without a display, use the Agg backend and save to files instead: fig.savefig("plot.png").

Setting the backend after importing pyplot. The matplotlib.use() call must come before import matplotlib.pyplot. Once pyplot is imported, the backend is locked.

Mixing plt.ion() with FuncAnimation. FuncAnimation manages its own event loop via plt.show(). Enabling interactive mode can interfere with the animation timer. Use one approach or the other, not both.

Not keeping a reference to the animation object. If the FuncAnimation object gets garbage collected, the animation stops. Always assign it to a variable that stays in scope: ani = animation.FuncAnimation(...).

Summary

Use plt.ion() with plt.pause() for the simplest non-blocking Matplotlib workflow. Update existing line objects with set_data() instead of calling plot() repeatedly. For high-performance updates, use blitting or FuncAnimation. Verify your backend is interactive (not Agg). Keep all Matplotlib calls on the main thread when using multithreaded applications. In Jupyter, use %matplotlib widget with the ipympl package for interactive in-notebook plots.


Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

All Rights Reserved.