pyplot vs Object-Oriented API
In one line:
plt.functions act on a hidden "current Axes"; the OO API acts on an Axes you name — use the OO API for anything you will keep.
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:
def volcano(df, ax=None, fc_thresh=1, p_thresh=0.05):
"""Draw a volcano plot. Returns the Axes."""
if ax is None:
_, ax = plt.subplots(figsize=(5, 4))
sig = (df["padj"] < p_thresh) & (df["log2fc"].abs() > fc_thresh)
ax.scatter(df.loc[~sig, "log2fc"], df.loc[~sig, "neglog10p"],
s=6, c="lightgrey", rasterized=True)
ax.scatter(df.loc[sig, "log2fc"], df.loc[sig, "neglog10p"], s=8, c="crimson")
ax.axvline(0, c="grey", lw=0.8)
ax.set(xlabel="log$_2$ fold change", ylabel="$-$log$_{10}$ adjusted $p$")
return ax
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