Matplotlib plots keep overlapping? Switch to the object-oriented API
Use Matplotlib's Figure and Axes objects explicitly to stop plots from leaking state across loops and figures.
27 Jul 2025, 01:07 UTC

Your loop is drawing yesterday’s data on today’s chart
You write a script that generates one chart per experiment. The first chart looks right. The second chart contains lines from the first experiment, plus the new ones. By the third chart, the figure is a mess.
This usually happens because Matplotlib’s pyplot interface keeps a global state: the "current figure" and the "current axes." When you call plt.plot(), Matplotlib draws on whatever axes it thinks is active, even if you created a new figure. In short scripts this is convenient. In loops, tests, or batch jobs, it is a source of silent bugs.
The fix: work with Figure and Axes objects directly
The object-oriented API makes the target explicit. A Figure is the whole window or image file. An Axes is a single plot area with its own x- and y-axes. Instead of asking Matplotlib where to draw, you pass the exact Axes object to the plotting call.
plt.subplots() creates both and returns them. You then call ax.plot(), ax.set_xlabel(), and so on. Because the Axes reference is a normal Python variable, it cannot be overwritten by a later plt call in the same loop.
Worked example: reusable chart function
Run this in a local Python environment or Jupyter notebook. No administrator permissions are required.
import matplotlib.pyplot as plt
import numpy as np
def plot_trial(ax, times, values, trial_name):
"""Draw one trial on the Axes object passed in."""
ax.plot(times, values, label=trial_name)
ax.set_xlabel("Time (s)")
ax.set_ylabel("Voltage (mV)")
ax.set_title(trial_name)
ax.legend(loc="upper right")
times = np.linspace(0, 10, 200)
for trial in ["baseline", "treatment_a", "treatment_b"]:
values = np.sin(times) + np.random.normal(0, 0.1, size=times.shape)
# Create a fresh Figure and Axes for every trial
fig, ax = plt.subplots(figsize=(6, 4))
plot_trial(ax, times, values, trial)
fig.savefig(f"{trial}.png", dpi=150, bbox_inches="tight")
# Release the figure from memory immediately
plt.close(fig)
Expected check: each PNG should contain exactly one trace and only that trial’s title. If you remove plt.close(fig) and run this inside a long-running process, memory usage grows because Matplotlib retains every Figure object until the process ends.
Trade-offs and when pyplot still makes sense
The object-oriented style is more verbose. For a one-line exploration in a notebook, plt.plot([1, 2, 3]) is faster to type. The risk appears when code moves from exploration to production: mixing plt.plot() with ax.plot() in the same script can draw on the wrong axes without raising an error.
Use the object-oriented API when you write functions, generate multiple figures, build GUIs, or run batch jobs. Keep pyplot for quick interactive checks where you are the only consumer of the output.
How to verify your script is safe
- Run your plotting loop and inspect every output file. No line or label from trial N should appear in trial N+1.
- Search your codebase for
plt.plot,plt.title, and similar state-based calls inside functions that accept anaxargument. Replace them with the correspondingax.*method. - If the script runs repeatedly, watch process memory or add
plt.close(fig)after saving or displaying each figure.
Closing
Matplotlib’s state machine is not broken; it is designed for interactive use. Treat pyplot as a scratchpad and pass Axes objects explicitly when the code needs to be predictable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.