PYTHON / MATPLOTLIB
Figures, axes, and the object-oriented API
Create Matplotlib figures and axes explicitly, know which object owns which method, and stop relying on pyplot's hidden current-axes state.
What you will learn
- Unpack fig, ax = plt.subplots() and call plotting methods on ax directly
- Translate plt.title/plt.xlabel into ax.set_title/ax.set_xlabel
- Pass an ax into helper functions so drawing code works in any figure
- Inspect fig.axes, ax.figure, plt.gcf() and plt.gca() to see pyplot's state
Understanding Figures, axes, and the object-oriented API
A Figure is the whole image you will eventually save: it owns a size in inches, a background, and a list of child Axes. An Axes is one rectangular plotting region with its own data limits, ticks, title and legend. The name is confusing because Axes is not plural of axis; the x and y rulers inside an Axes are separate Axis objects reachable as ax.xaxis and ax.yaxis. Almost everything you draw is a method on an Axes, and every Axes knows its parent through ax.figure.
The pyplot functions you see in short snippets are a thin state-machine layer over that object graph. plt.plot(...) is roughly plt.gca().plot(...), and plt.gca() means "the current Axes of the current Figure, creating either one if it does not exist yet". That is why a bare plt.plot works with no setup, and also why the same call behaves differently depending on which figure was touched last. The state lives in a module-level registry, not in your variables.
Working object-oriented means you hold the references yourself: fig, ax = plt.subplots() gives you both, and from then on nothing depends on ordering or on what pyplot considers current. The method names shift from bare setters to explicit ones, because on an Axes the attribute names are already taken by objects: plt.title becomes ax.set_title, plt.xlim becomes ax.set_xlim, plt.xlabel becomes ax.set_xlabel. This is also what lets you write a function that accepts an ax argument, so the same drawing code can be reused in a one-panel figure or dropped into any panel of a larger one.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from matplotlib.axes import Axes
fig, ax = plt.subplots(figsize=(4, 3))
ax.plot([0, 1, 2], [0, 1, 4])
ax.set_title("squares")
print(isinstance(fig, Figure), isinstance(ax, Axes))
print(ax.figure is fig)
print(len(fig.axes), fig.axes[0] is ax)
print(ax.get_title())
print(plt.gca() is ax, plt.gcf() is fig)
print(ax.get_xlabel() == "")A Figure contains Axes, every plotting command is really a method on one specific Axes, and pyplot only guesses which Axes you meant.
Worked examples
What pyplot is really doing
Shows that plt.plot creates and targets a current Axes, and that opening a new figure silently redirects later calls.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
print(len(plt.gcf().axes))
plt.plot([1, 2, 3])
first = plt.gca()
print(len(plt.gcf().axes))
plt.figure()
print(plt.gcf() is first.figure)
plt.plot([3, 2, 1])
print(len(first.lines), len(plt.gca().lines))
print(len(plt.get_fignums()))Example explained
Line 1plt.gcf() creates an empty Figure on demand, so it starts with zero Axes.
Line 2plt.plot has to draw somewhere, so it calls gca(), which adds a full-size Axes to that figure.
Line 3plt.figure() makes a second figure current, so plt.gcf() is no longer the figure holding first.
Line 4The second plt.plot lands in the new figure's Axes; first keeps its single line, and two figure numbers are now registered.
A drawing function that takes an ax
Builds two Axes on one Figure and reuses the same helper on each by passing the target Axes explicitly.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
def draw_squares(ax, n):
xs = list(range(n))
ax.plot(xs, [x * x for x in xs])
ax.set_ylabel("x squared")
fig = plt.figure(figsize=(6, 3))
left = fig.add_subplot(1, 2, 1)
right = fig.add_subplot(1, 2, 2)
draw_squares(left, 4)
draw_squares(right, 8)
print(len(fig.axes))
print(left is fig.axes[0], right is fig.axes[1])
print(left.get_ylabel(), right.get_ylabel())
print(len(left.get_lines()[0].get_xdata()), len(right.get_lines()[0].get_xdata()))Example explained
Line 1draw_squares never mentions plt, so it cannot accidentally draw into the wrong panel.
Line 2fig.add_subplot(1, 2, 1) attaches an Axes to fig in a one-row, two-column grid and returns it.
Line 3fig.axes lists the Axes in creation order, which is why left and right match index 0 and 1.
Line 4Each Axes stores its own label and its own Line2D, so the two panels hold 4 and 8 x values.
Important notes
plt.subplots() with no arguments still returns a single Axes, not an array; you only get an array once you ask for more than one row or column.
fig.add_axes([0.1, 0.1, 0.8, 0.8]) places an Axes at explicit figure-fraction coordinates, while add_subplot places it in a grid cell; both end up in fig.axes.
Common mistakes
Creating two Axes and then calling plt.xlabel("time"): the label goes to whichever Axes pyplot last made current, so it silently appears on one panel only.
Translating plt.title to ax.title: ax.title is a Text object, so assigning to it or calling it fails or breaks rendering; the setter is ax.set_title.
Calling plt.subplot instead of plt.subplots and then unpacking: plt.subplot returns a single Axes, so fig, ax = plt.subplot(...) raises a TypeError about unpacking a non-iterable Axes.
Creating a new figure inside a loop without fig.clf() or plt.close(fig): the figures stay registered in pyplot, and after twenty you get a RuntimeWarning and growing memory use.
Try it yourself
Change, predict, then run
Build one Figure with two Axes using fig.add_subplot, write a function plot_line(ax, values) that plots the values and sets a title from len(values), call it on both Axes with different lists, then print len(fig.axes) and whether each Axes reports ax.figure is fig.
Open the Python workspaceCheck your understanding
You run fig, (ax1, ax2) = plt.subplots(1, 2), plot into ax1, plot into ax2, then call plt.ylabel("count"). Where does the label appear?
- On ax2, because pyplot applies it to the current Axes, which is the one most recently created or selected
- On both ax1 and ax2, because plt functions apply to the whole figure
- On ax1, because it was the first Axes created and plotted into
- Nowhere, because plt.ylabel raises an error when a figure has more than one Axes
Show answer
plt.ylabel is shorthand for plt.gca().set_ylabel(), and gca() returns the current Axes; plt.subplots sets the last created Axes as current, so the label lands on ax2 alone. The "both Axes" option is tempting because plt looks figure-level, but pyplot state points at exactly one Axes, which is why explicit ax1.set_ylabel is the reliable form.