Start hereQuick start · 6 you’ll reach for daily
Target versions · paceVersions
matplotlib ≥ 3.9
numpy ≥ 1.26
python ≥ 3.10
Default to the object-oriented API (fig, ax = plt.subplots()),
not the implicit plt.* globals — the OO API is what every modern matplotlib doc page assumes.
layout="constrained" supersedes tight_layout and handles colorbars + suptitles correctly.
Style sheets named seaborn-* are now seaborn-v0_8-*.
This sheet pins to matplotlib 3.9+.
Install · backendSetup
# Install — core + bundled deps (numpy comes along)
pip install "matplotlib>=3.9"
# Optional but useful
pip install ipympl # interactive backend for Jupyter
pip install seaborn # statistical wrapper on top
pip install pillow # for image I/O via imread
# Conda — picks a sane backend for your OS
conda install -c conda-forge matplotlib
# Pick a backend before importing pyplot (only if the default fights you)
import matplotlib
matplotlib.use("Agg") # non-GUI: scripts, CI, servers
# matplotlib.use("TkAgg") # native window
# matplotlib.use("module://matplotlib_inline.backend_inline") # Jupyter
# In Jupyter — choose one per notebook
# %matplotlib inline # static PNGs (default)
# %matplotlib widget # interactive (needs ipympl)
# Confirm
python -c "import matplotlib as m; print(m.__version__, m.get_backend())"
Where things liveCommon imports
| import matplotlib.pyplot as plt | Canonical alias. Entry point for figure / axes creation. |
| import matplotlib as mpl | Top-level — rcParams, backend, version. |
| import matplotlib.dates as mdates | Date tick locators / formatters. |
| import matplotlib.ticker as mticker | Custom number tick formatters (FuncFormatter, PercentFormatter). |
| import matplotlib.patches as mpatches | Rectangle, Circle, FancyArrowPatch — for annotations. |
| from matplotlib.colors import LogNorm, Normalize, ListedColormap | Color scales for heatmaps / images. |
| from matplotlib.gridspec import GridSpec | Fine-grained subplot layouts. |
Explicit > implicitAPI style: OO vs pyplot
| fig, ax = plt.subplots() | Make a figure + one axes. Preferred OO entry point. |
| fig, axes = plt.subplots(2, 3, figsize=(10, 6)) | Grid — axes is a 2x3 ndarray of Axes. |
| ax.plot(x, y) / ax.scatter(x, y) | Call methods on the axes. Composable. |
| plt.plot(x, y) / plt.title("...") | Legacy implicit API. Operates on "current" figure. |
| fig.suptitle("Figure title") | Title above the entire figure. |
| ax.set(title="...", xlabel="...", ylabel="...", xlim=(0,10)) | One-shot setter — tidier than four lines. |
| fig.tight_layout() vs layout="constrained" | Auto-fix overlap. constrained handles colorbars correctly. |
| plt.close(fig) / plt.close("all") | Free figures in scripts / loops. Otherwise they leak. |
| plt.show() | Render + block (GUI backends). No-op in Jupyter / Agg. |
line · scatter · hist · ...Plot types
| ax.plot(x, y, "o-", label="...") | Line. Format string = marker + linestyle. |
| ax.scatter(x, y, s=size, c=value, cmap="viridis", alpha=0.6) | Scatter with size + color encoding. |
| ax.bar(cats, heights, yerr=errs) / ax.barh(...) | Vertical / horizontal bars. |
| ax.hist(x, bins=30, density=True, range=(0,1)) | Histogram. density=True integrates to 1. |
| ax.hist2d(x, y, bins=50, cmap="magma") | 2-D histogram. Heatmap of counts. |
| ax.boxplot([a, b, c], labels=["a","b","c"]) | Boxplot. |
| ax.violinplot([a, b, c], showmedians=True) | Distribution shape per group. |
| ax.errorbar(x, y, yerr=err, fmt="o", capsize=3) | Mean + error bars. |
| ax.fill_between(x, lo, hi, alpha=0.2) | Confidence band. |
| ax.step(x, y, where="post") | Step plot — cumulative / staircase data. |
| ax.imshow(M, cmap="gray", aspect="auto", origin="lower") | Matrix / image heatmap. |
| ax.contour(X, Y, Z, levels=10) / ax.contourf(...) | Iso-line / filled contours. |
| ax.quiver(X, Y, U, V) / ax.streamplot(X, Y, U, V) | Vector / streamline fields. |
limits · ticks · logAxes, scales, ticks
| ax.set_xlim(0, 10) / ax.set_ylim(-1, 1) | Numeric limits. |
| ax.set_xscale("log") / ax.set_yscale("symlog") | Log / symlog axes. |
| ax.set_xticks([0, 1, 2], labels=["a","b","c"]) | Custom tick positions + labels. |
| ax.xaxis.set_major_locator(mticker.MultipleLocator(5)) | Tick every N units. |
| ax.xaxis.set_major_formatter("{x:,.0f}") | f-string formatter shortcut. |
| ax.tick_params(axis="x", labelrotation=45, labelsize=9) | Bulk tweak tick style. |
| ax.invert_yaxis() / ax.invert_xaxis() | Flip an axis direction. |
| ax.spines[["top","right"]].set_visible(False) | Hide axis spines — cleaner look. |
| ax2 = ax.twinx() / twiny() | Secondary y / x sharing the other axis. |
| ax.axhline(y, color="grey", linestyle="--") | Horizontal reference line. |
| ax.axvspan(start, end, alpha=0.2) | Vertical band — mark a date range. |
| ax.grid(True, which="major", alpha=0.3) | Grid toggling. |
Grids · mosaic · gridspecSubplots & layout
| plt.subplots(2, 3, figsize=(12, 6), sharex=True) | Grid with shared x. |
| plt.subplots(..., layout="constrained") | Auto-fix overlap. Preferred over tight_layout. |
| plt.subplot_mosaic([["a","b"], ["a","c"]]) | ASCII layout: a spans the left column, b + c right. |
| fig.add_gridspec(3, 3) + fig.add_subplot(gs[:2, :2]) | Slice-based subplot placement. |
| ax.inset_axes([0.6, 0.6, 0.35, 0.35]) | Inset axes in figure-fraction coords. |
| fig.subplots_adjust(left=..., right=..., wspace=0.3, hspace=0.4) | Manual padding when auto-layout isn’t enough. |
| fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04) | Attach a colorbar to a specific axes. |
| fig.legend(handles, labels, loc="upper center", ncols=4) | Figure-level legend (vs per-axes). |
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 2 * np.pi, 200)
# Single figure, 2x2 grid — shared y, constrained layout
fig, axes = plt.subplots(
nrows=2, ncols=2, figsize=(8, 6),
sharex=True, sharey="row", # share within rows
layout="constrained", # auto-fix overlap; replaces tight_layout
)
axes[0, 0].plot(x, np.sin(x)); axes[0, 0].set_title("sin")
axes[0, 1].plot(x, np.cos(x)); axes[0, 1].set_title("cos")
axes[1, 0].plot(x, np.tan(x)); axes[1, 0].set_title("tan"); axes[1, 0].set_ylim(-5, 5)
axes[1, 1].plot(x, x**2); axes[1, 1].set_title("x^2")
# Uneven layout — subplot_mosaic. ASCII map is the layout.
fig, axd = plt.subplot_mosaic(
[["top", "top"],
["left", "right"]],
figsize=(8, 6),
layout="constrained",
)
axd["top"].plot(x, np.sin(x))
axd["left"].hist(np.random.default_rng(0).standard_normal(1000), bins=30)
axd["right"].scatter(x, np.sin(x) + np.random.default_rng(0).normal(0, 0.1, 200))
# GridSpec — manual control when mosaic isn't enough
fig = plt.figure(figsize=(8, 6), layout="constrained")
gs = fig.add_gridspec(3, 3)
ax_big = fig.add_subplot(gs[:2, :2])
ax_right = fig.add_subplot(gs[:2, 2])
ax_bot = fig.add_subplot(gs[2, :])
Styles · rcParams · colorsStyling
| plt.style.use("seaborn-v0_8-whitegrid") | Builtin style sheet. |
| plt.style.available | List installed styles. |
| plt.style.use(["seaborn-v0_8-paper", "mystyle.mplstyle"]) | Stack styles — last wins. |
| with plt.style.context("dark_background"): ... | Temporary, scoped style. |
| plt.rcParams["font.size"] = 11 | Tweak any default. |
| mpl.rc("axes", titlesize=14, labelsize=12) | Group setter for an rcParam family. |
| color="C0" / color="#ff6b6b" / color=(0.2, 0.4, 0.6, 0.8) | Cycle slot / hex / RGBA. |
| linewidth, linestyle, marker, markersize, alpha, zorder | Per-line aesthetics. |
| cycler import cycler → ax.set_prop_cycle(...) | Custom color / linestyle cycle. |
| ax.annotate("peak", xy=(x, y), xytext=(...), arrowprops=...) | Annotations with arrows. |
import matplotlib.pyplot as plt
import numpy as np
# Style sheet — applies to all plots in this process
plt.style.use("seaborn-v0_8-whitegrid") # builtin; many others available
# plt.style.available → list them
# Or one-off rcParams overrides
plt.rcParams.update({
"figure.dpi": 120,
"savefig.dpi": 200,
"font.size": 11,
"axes.titlesize": 13,
"axes.spines.top": False,
"axes.spines.right": False,
"legend.frameon": False,
})
# Context manager — temporary style (doesn't leak)
with plt.style.context("dark_background"):
fig, ax = plt.subplots()
ax.plot([0, 1, 2], [0, 1, 4])
# Colors: cycle, named, hex, rgb, alpha
rng = np.random.default_rng(0)
fig, ax = plt.subplots()
for i in range(4):
ax.plot(rng.standard_normal(50).cumsum(),
color=f"C{i}", # cycle slot (C0..C9)
linewidth=1.5, alpha=0.85,
label=f"series {i}")
ax.legend(loc="upper left", ncols=2)
# Per-line cosmetics — short codes still work
ax.plot([0,1,2], [0,1,4], "o--", color="#ff6b6b", markersize=6, markeredgewidth=0)
# Annotate a point with an arrow
ax.annotate("peak", xy=(2, 4), xytext=(1, 3.5),
arrowprops=dict(arrowstyle="->", color="grey"))
cmap · normColors & colormaps
| cmap="viridis" | Default sequential. Perceptually uniform. |
| cmap="magma" / "inferno" / "cividis" / "plasma" | Other sequential cmaps. All perceptually uniform. |
| cmap="coolwarm" / "RdBu_r" / "bwr" | Diverging — for signed values around a center. |
| cmap="tab10" / "tab20" | Qualitative — for unordered categories. Never use a sequential cmap here. |
| norm=Normalize(vmin=0, vmax=1) / LogNorm(vmin=1e-3, vmax=1) | Linear / log color mapping. |
| norm=TwoSlopeNorm(vmin=-1, vcenter=0, vmax=2) | Asymmetric diverging map around a center. |
| fig.colorbar(im, ax=ax, label="...", extend="both") | Attach a colorbar; extend shows out-of-range arrows. |
| mpl.colormaps["viridis"].resampled(8) | Discretize a continuous cmap into N bins. |
PNG · PDF · SVGSaving figures
| fig.savefig("plot.png", dpi=200, bbox_inches="tight") | Raster output. bbox_inches="tight" trims whitespace. |
| fig.savefig("plot.pdf", bbox_inches="tight") | Vector PDF — Preferred for print / publications. |
| fig.savefig("plot.svg") | Vector SVG — for web. |
| fig.savefig(..., transparent=True) | No background — useful over branded slides. |
| fig.savefig(..., metadata={"Author":"...", "Title":"..."}) | Embed metadata (PDF/PNG only). |
| with PdfPages("report.pdf") as pdf: pdf.savefig(fig) | Multi-page PDF — one figure per page. |
| plt.rcParams["savefig.dpi"] = 200 | Default DPI for every save call. |
| plt.close(fig) / plt.close("all") | Always close after saving in a script — figures leak otherwise. |
df.plot(ax=ax, ...)Pandas integration
| df.plot(ax=ax, kind="line", x="ts", y=["a","b"]) | Pass an Axes to render into your figure. |
| df["x"].hist(ax=ax, bins=30) | Series shortcut for histograms. |
| df.plot.scatter(x="a", y="b", c="cat", colormap="viridis") | Color by a column. |
| df.plot(kind="bar", stacked=True) | Stacked categorical bars. |
| df.boxplot(column=["a","b"], by="group") | Boxplot per group. |
| from pandas.plotting import scatter_matrix; scatter_matrix(df, diagonal="kde") | Quick pairs plot. |
.plot returns the underlying Axes — capture it with ax = df.plot(...) and continue styling with matplotlib’s OO API.
mplot3d · FuncAnimation3D & animation
| fig.add_subplot(projection="3d") | 3D axes. From mpl_toolkits.mplot3d — auto-loaded. |
| ax3.plot_surface(X, Y, Z, cmap="viridis", alpha=0.8) | Surface plot. |
| ax3.scatter(xs, ys, zs, c=values) | 3D scatter. |
| ax3.view_init(elev=20, azim=-60) | Camera angle. |
| from matplotlib.animation import FuncAnimation | Frame-based animations. |
| FuncAnimation(fig, update, frames=range(100), interval=50) | Calls update(i) each frame. |
| ani.save("a.mp4", writer="ffmpeg", fps=30) | Needs ffmpeg installed. |
| ani.save("a.gif", writer="pillow") | GIF output via Pillow. |
Time series · dual y · PDFEnd-to-end · Price + volume chart
Object-oriented API end to end — twin y-axis, date tick formatter, merged legends, vector + raster save.
# Time-series chart, publication-ready — title, secondary y, formatted ticks, PDF save.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import pandas as pd
rng = np.random.default_rng(0)
idx = pd.date_range("2026-01-01", periods=180, freq="D")
price = 100 + rng.standard_normal(180).cumsum()
volume = rng.integers(1_000, 5_000, 180)
fig, ax = plt.subplots(figsize=(9, 4.5), layout="constrained")
ax.plot(idx, price, color="C0", linewidth=1.5, label="Price")
ax.set_ylabel("Price (USD)")
ax.set_title("Daily price + volume, 2026 H1")
# Secondary y for volume — twinx shares the x axis
ax2 = ax.twinx()
ax2.bar(idx, volume, alpha=0.25, color="C1", label="Volume")
ax2.set_ylabel("Volume")
# Date ticks — major month, minor day
ax.xaxis.set_major_locator(mdates.MonthLocator())
ax.xaxis.set_major_formatter(mdates.DateFormatter("%b"))
ax.xaxis.set_minor_locator(mdates.DayLocator())
# Merge legends from both axes
h1, l1 = ax.get_legend_handles_labels()
h2, l2 = ax2.get_legend_handles_labels()
ax.legend(h1 + h2, l1 + l2, loc="upper left", frameon=False)
# Save — bbox_inches="tight" trims whitespace
fig.savefig("price_volume.pdf", bbox_inches="tight")
fig.savefig("price_volume.png", dpi=200, bbox_inches="tight")
plt.close(fig)
Best practiceGood to know
fig, ax = plt.subplots()).
The implicit plt.plot() / plt.title() globals operate on a hidden "current" figure — fine for quick REPL plots, painful in any function or notebook that builds multiple figures.
layout="constrained", not tight_layout().
Constrained layout handles colorbars, suptitles, and legends without the surprise overlaps tight_layout still produces.
viridis, magma).
jet and rainbow mislead readers and break for color-blind viewers. The default cmap exists for a reason — use it unless you have a specific reason not to.
Common trapsWatch out for
plt.close(fig) in scripts leaks figures.
Every plt.subplots() retains a reference until the process exits. In a loop over thousands of items, that’s a real memory leak. Always close after saving.
ax.imshow defaults to origin="upper".
Pixel (0, 0) goes to the top-left, like an image — not the mathematical convention. Pass origin="lower" for heatmaps of matrices.
seaborn-darkgrid error in 3.9+.
They were renamed to seaborn-v0_8-darkgrid. Use plt.style.available to discover the current names.