Pyplot vs Object-Oriented API
pyplot vs Object-Oriented API
Core Principle: Production code should utilize Matplotlib's object-oriented API by manipulating explicit
FigureandAxesinstances rather than relying on statefulpltfunctions that manage hidden global context.
The two styles
# implicit (pyplot state machine): MATLAB heritage
plt.figure(figsize=(6, 4))
plt.scatter(x, y)
plt.xlabel("log2 fold change")
plt.title("Volcano")
plt.savefig("out.pdf")
# explicit (object-oriented): use this
fig, ax = plt.subplots(figsize=(6, 4))
ax.scatter(x, y)
ax.set_xlabel("log2 fold change")
ax.set_title("Volcano")
fig.savefig("out.pdf")
Identical output. Different failure modes.
Why the implicit style breaks
plt.xlabel(...) labels whatever the current Axes happens to be. That is fine for one plot in one cell and unreliable everywhere else:
fig1, ax1 = plt.subplots()
ax1.plot(a)
fig2, ax2 = plt.subplots() # fig2 is now "current"
ax2.plot(b)
plt.title("Title for figure 1?") # ← lands on fig2
The same problem appears in a loop, in a function that plots and returns, in a notebook where cells run out of order, and any time a library (including seaborn) creates a Figure behind your back.
The OO style has no hidden state. ax.set_title(...) titles that Axes, always.
The translation table
| pyplot | OO |
|---|---|
plt.plot(x, y) |
ax.plot(x, y) |
plt.xlabel(s) |
ax.set_xlabel(s) |
plt.ylabel(s) |
ax.set_ylabel(s) |
plt.title(s) |
ax.set_title(s) |
plt.xlim(a, b) |
ax.set_xlim(a, b) |
plt.xscale("log") |
ax.set_xscale("log") |
plt.xticks(t, l) |
ax.set_xticks(t) + ax.set_xticklabels(l) |
plt.legend() |
ax.legend() |
plt.grid() |
ax.grid() |
plt.axhline(0) |
ax.axhline(0) |
plt.suptitle(s) |
fig.suptitle(s) |
plt.savefig(p) |
fig.savefig(p) |
plt.colorbar() |
fig.colorbar(m, ax=ax) |
plt.tight_layout() |
fig.tight_layout() |
The pattern: set_ prefix on the Axes, because these are property setters on an object rather than commands to a state machine.
What pyplot is still for
You do need plt for a few things: it is the entry point, not the enemy:
import matplotlib.pyplot as plt
plt.subplots(...) # creating figures: the standard entry point
plt.show() # display
plt.close(fig); plt.close("all")
plt.style.use("seaborn-v0_8-whitegrid")
plt.rcParams["font.size"] = 10
plt.get_cmap("viridis")
So the idiomatic modern style is: plt.subplots() to create, then OO for everything after.
Interaction with seaborn
Axes-level seaborn functions take ax= and return the Axes, which fits the OO style exactly:
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
sns.scatterplot(data=df, x="a", y="b", ax=axes[0])
sns.histplot(data=df, x="a", ax=axes[1])
axes[0].axhline(0, ls="--", c="grey")
fig.savefig("out.pdf", bbox_inches="tight")
Figure-level functions create their own Figure, so you reach in afterwards:
g = sns.relplot(data=df, x="a", y="b", col="gene", col_wrap=3)
for ax in g.axes.flat:
ax.axhline(0, ls="--", c="grey")
g.figure.savefig("out.pdf")
See Figure-level vs Axes-level.
Writing plotting functions
The OO style makes plots composable. The convention worth adopting:
This code completed without producing output.
The ax=None pattern is the standard. It lets the function be used standalone and dropped into a panel of a larger figure:
fig, axes = plt.subplots(1, 3, figsize=(14, 4), sharey=True)
for ax, (name, sub) in zip(axes, results.items()):
volcano(sub, ax=ax)
ax.set_title(name)
A function that calls plt.figure() internally can never be composed this way. This alone is a sufficient reason to learn the OO API.
Backends
matplotlib.get_backend()
matplotlib.use("Agg") # headless: must be set BEFORE importing pyplot
%matplotlib inline # Jupyter static
%matplotlib widget # Jupyter interactive
Agg is what you want in a script, a container, or a cluster job. Without it, a plt.show() on a headless machine either errors or hangs.
Common mistakes
- Mixing the two styles and labelling the wrong Axes.
plt.calls after seaborn created a Figure you did not.- Functions that call
plt.figure()internally: uncomposable. - Not closing figures in a loop.
- Forgetting
set_:ax.xlabel(...)is an AttributeError. matplotlib.use("Agg")after importing pyplot. Too late; it must come first.plt.show()in a script that also saves. Blocks, and on some backends clears the figure.
See also
Figure and Axes · Matplotlib Artists · Figure-level vs Axes-level · Saving Figures · Subplots and Layout
Exercise
Write build_panels(suptitle, specs). The first input line is the figure's
overall title; every later line is a panel written as
title|xlabel|v1 v2 v3. Build a single row of panels with
plt.subplots(..., squeeze=False) and fill each one using the object-oriented
API only: ax.plot, ax.set_title, ax.set_xlabel and ax.set_ylim(bottom=0)
on the Axes that panel owns, then fig.suptitle on the Figure. The pyplot
state machine would attach every one of those to whichever Axes happened to be
current, so the readback below would show all the titles stacked on the last
panel.
Return suptitle=<text> first, then one line per panel, in grid order:
<index> <title> | <xlabel> | n=<count> | last=<value> | bottom=<value>. The
count and the final y value must come from ax.lines[0].get_ydata() and the
bottom limit from ax.get_ylim(), both rounded to three decimals: read them
back off the Axes rather than echoing the input. A single panel is a legitimate
input and must produce exactly two lines of output.
Test cases · 3
| # | via | input | expected stdout |
|---|---|---|---|
| 1 | stdin | Quality control Depth|sample|10 20 30 GC|sample|0.41 0.43 0.39 Duplication|sample|0.12 0.15 0.11 | suptitle=Quality control 0 Depth | sample | n=3 | last=30.0 | bottom=0.0 1 GC | sample | n=3 | last=0.39 | bottom=0.0 2 Duplication | sample | n=3 | last=0.11 | bottom=0.0 |
| 2 | stdin | Single panel Coverage|position|5 8 13 21 | suptitle=Single panel 0 Coverage | position | n=4 | last=21.0 | bottom=0.0 |
| 3 | stdin | Dose response TP53|dose|1.0 2.0 BRCA1|dose|0.5 0.25 | suptitle=Dose response 0 TP53 | dose | n=2 | last=2.0 | bottom=0.0 1 BRCA1 | dose | n=2 | last=0.25 | bottom=0.0 |
Hints · 3
01Hint
`plt.subplots(1, 1)` returns a bare Axes unless you pass `squeeze=False`.
02Hint
`ax.set_ylim(bottom=0)` fixes only the lower limit and leaves the upper one autoscaled.
03Hint
The suptitle is a Figure-level Text artist, so it lands in `fig.texts` rather than on any Axes.