pythonforbio.
[WASM idle]
Plotting11/14

pyplot vs Object-Oriented API

Starting sandbox…
Beginnerlesson

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

lesson example

No output yet — run the code to populate this drawer.