cheat sheet

matplotlib

Package-level reference for matplotlib on PyPI — install variants, backends, version policy, extras, and alternatives.

matplotlib

What it is

matplotlib is the foundational 2-D plotting library for Python, originally written by John D. Hunter in 2003 to bring MATLAB-style plotting to a free toolchain. It is now stewarded by NumFOCUS and a wide maintainer pool, and forms the rendering backbone for seaborn, pandas .plot(), and most scientific-Python tutorials.

Reach for matplotlib on PyPI when you need static figures saved to disk (PNG, PDF, SVG, EPS), fine-grained axis control, or a stable downstream target for higher-level libraries. Reach for plotly or bokeh when you need interactive browser-native charts; reach for altair / vega-lite when you want a declarative grammar.

Install

bash
pip install matplotlib

Output: (none — exits 0 on success)

bash
uv add matplotlib

Output: resolved + added to pyproject.toml

bash
poetry add matplotlib

Output: updated lockfile + virtualenv install

bash
conda install -c conda-forge matplotlib

Output: installs matplotlib plus a pre-built Qt/Tk backend stack — preferred on Windows where wheel-built backends sometimes lag.

Versioning & Python support

  • Current stable line is the 3.x series; releases are roughly twice a year with a long deprecation window.
  • Supports Python 3.10+ on recent releases. Older releases support Python 3.9 and earlier; check the changelog for the exact floor.
  • Loose semver — 3.x minor releases may remove APIs that were deprecated in two prior releases (typically ~1 year notice).
  • A future 4.0 has been discussed but is not yet on a release schedule as of late 2025.

Package metadata

  • Maintainer: Matplotlib Development Team (NumFOCUS fiscally sponsored)
  • Project home: github.com/matplotlib/matplotlib
  • Docs: matplotlib.org/stable
  • PyPI: pypi.org/project/matplotlib
  • License: Matplotlib License (BSD-style, PSF-derived)
  • Governance: NumFOCUS fiscally sponsored project; consensus-based core team
  • First released: 2003
  • Downloads: tens of millions per month — consistently in the PyPI top 30

Optional dependencies & extras

matplotlib has no PyPI-declared extras_require markers in the usual sense, but its functionality is heavily gated by which backend is available on your system:

  • Agg (default, always available) — non-interactive PNG rendering. Used by savefig() and headless CI.
  • TkAgg — interactive plt.show() window via stdlib tkinter. Works out of the box on most desktop installs.
  • QtAgg / Qt5Agg / Qt6Agg — needs pip install pyqt5 or pyqt6 for a modern GUI window with zoom/pan.
  • MacOSX — native Cocoa backend, only on macOS, only when matplotlib was built against it.
  • nbagg / ipympl — interactive backend for Jupyter; install pip install ipympl and use %matplotlib widget.
  • WebAgg — serves a live figure to a browser; useful for remote workflows.

Optional companions:

  • pillow — PIL/Pillow is required to write JPEG, TIFF, WebP, and a few other formats. matplotlib will fall through to it automatically when present.
  • numpy — hard dependency, installed automatically.
  • cycler, kiwisolver, pyparsing, python-dateutil, fonttools, packaging — all pulled in automatically.
  • latex (system binary) — needed only when text.usetex is enabled for LaTeX-rendered labels.

Alternatives

PackageTrade-off
seabornStatistical wrapper on matplotlib. Use for quick categorical + distribution plots; falls back to matplotlib for customization.
plotlyInteractive browser charts (WebGL). Use when readers click and zoom; bundle size is much larger.
bokehInteractive browser charts with a Python-server option. Use for streaming dashboards.
altairDeclarative grammar of graphics over vega-lite. Use when you prefer "describe the chart" over "draw the chart".
pygalSVG-only output, simple API. Use for lightweight reports where SVG embedding matters more than feature breadth.

Common gotchas

  1. Backend chaos. plt.show() behaviour depends entirely on the configured backend, which is auto-selected from matplotlibrc, the MPLBACKEND env var, and what is importable. On macOS the MacOSX backend conflicts with TkAgg if both are present. Set explicitly: import matplotlib; matplotlib.use("Agg") before import matplotlib.pyplot.
  2. Defaults are baked at import. rcParams reads are evaluated when modules import; setting plt.rcParams[...] after creating a figure usually has no effect for that figure. Set them at the top of the script or via a matplotlibrc file.
  3. %matplotlib inline vs widget in Jupyter. inline produces static PNGs; widget needs ipympl installed and a JupyterLab extension enabled. Switching mid-notebook leaves stale display handles — restart the kernel after changing.
  4. Mixing with seaborn / plotly resets the style. import seaborn calls seaborn.set_theme() as a side-effect on older versions; it overwrites your rcParams. Pin the import order or call sns.reset_defaults().
  5. Animation API churn. matplotlib.animation.FuncAnimation quietly changed argument semantics across 3.x minor releases (especially around cache_frame_data and blit). Pin matplotlib if you have CI-stable animations.
  6. Memory leaks from plt.figure() without plt.close(). In long-running scripts and notebooks, every figure stays referenced by the pyplot state machine until explicitly closed. Use the object-oriented API (fig, ax = plt.subplots()) and call plt.close(fig) when done.
  7. JPEG and WebP need Pillow. savefig("out.jpg") raises if Pillow is not installed; the error message is opaque ("Format 'jpg' is not supported"). Install Pillow.

Real-world recipes

These are package-level recipes — patterns where the install/runtime configuration matters more than the plotting API. For pure API examples (subplots, twin axes, basic chart types) see the companion Python article.

Recipe 1 — headless CI rendering with deterministic output

CI runners often have no display server. Pin the backend explicitly so a stale local MPLBACKEND env var can't leak into the build.

python
import matplotlib
matplotlib.use("Agg")            # raster-only, no GUI
import matplotlib.pyplot as plt
plt.rcParams["svg.hashsalt"] = "ci"     # deterministic SVG IDs across runs

fig, ax = plt.subplots(figsize=(6, 4), dpi=150)
ax.plot([0, 1, 2], [0, 1, 4])
fig.savefig("artifact.svg", bbox_inches="tight")
plt.close(fig)

Output: artifact.svg with stable element IDs — diff-friendly snapshot testing works because the random-salt seed is fixed.

Recipe 2 — a project-level style sheet

A ~/.matplotlib/stylelib/myproject.mplstyle file lives outside the repo and applies project-wide:

ini
# stylelib/myproject.mplstyle
font.family       : DejaVu Sans
axes.titlesize    : 14
axes.labelsize    : 11
axes.spines.top   : False
axes.spines.right : False
figure.dpi        : 110
savefig.dpi       : 200
savefig.bbox      : tight

Apply per-script:

python
import matplotlib.pyplot as plt
plt.style.use("myproject")     # or: plt.style.use(["seaborn-v0_8", "myproject"])

Output: every figure in the process uses the consolidated style — useful for report-grade consistency.

Recipe 3 — exporting a multi-format figure for a print + web pipeline

python
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(7, 4))
ax.bar(["A", "B", "C"], [3, 7, 5])

for fmt, kwargs in [
    ("png",  {"dpi": 220}),                                # web
    ("pdf",  {}),                                          # vector, print
    ("svg",  {}),                                          # vector, web
    ("webp", {"dpi": 160}),                                # smaller web PNG
]:
    fig.savefig(f"chart.{fmt}", bbox_inches="tight", **kwargs)

Output: four files; pdf and svg are vector (resolution-independent), png and webp are raster.

Recipe 4 — frame-by-frame animation with FuncAnimation

python
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np

fig, ax = plt.subplots()
line, = ax.plot([], [])
ax.set_xlim(0, 2 * np.pi)
ax.set_ylim(-1.1, 1.1)

x = np.linspace(0, 2 * np.pi, 200)

def update(frame: int):
    line.set_data(x[:frame], np.sin(x[:frame]))
    return (line,)

anim = animation.FuncAnimation(
    fig, update, frames=len(x), interval=20, blit=True, cache_frame_data=False
)
anim.save("sine.mp4", writer="ffmpeg", dpi=120, fps=30)

Output: sine.mp4ffmpeg must be on PATH. blit=True is the single biggest perf win; cache_frame_data=False avoids matplotlib silently caching every frame in memory (a recurring leak in long animations).

Performance tuning

Most matplotlib pain at scale comes from one of three places: the backend, the figure-construction cost, and savefig I/O. Each has a knob.

Backend choice for batch rendering

For thousands of figures-to-disk, the Agg backend is several times faster than any interactive backend because it skips the event loop entirely:

python
import matplotlib
matplotlib.use("Agg")            # must come before pyplot import

Output: no plt.show() will open a window — every figure is raster-only. Use this in CI, schedulers, and reports.

Blitting for animations

blit=True redraws only the changed artists; without it, every frame re-renders the whole figure. The default is False for compatibility but it should almost always be True for live animation.

Reusing figures in a hot loop

Creating a figure costs ~30–80 ms (font cache, axes setup). For 10,000 small charts, reuse the figure and ax.clear() between iterations:

python
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
for i, row in enumerate(rows):
    ax.clear()
    ax.plot(row.x, row.y)
    fig.savefig(f"out/{i:05}.png", dpi=120)
plt.close(fig)

Output: each loop iteration costs the savefig cost only; the figure machinery is amortised.

Font cache rebuilds

First import after a font install triggers a font-cache rebuild that can take 10–30 s. To pre-warm in a container build step, run python -c "import matplotlib.pyplot" during image build so the cache lands in the layer rather than every container start.

Rasterise large scatter, keep the rest vector

For SVG/PDF output with millions of points, rasterized=True on the heavy artist keeps the axes/labels as vectors and rasterises only the scatter — files stay small and remain editable in Illustrator/Inkscape:

python
ax.scatter(x, y, s=2, rasterized=True)
fig.savefig("mixed.pdf", dpi=200)

Output: PDF with vector axes and a rasterised scatter at 200 DPI — both crisp text and a small file.

Version migration guide

matplotlib's deprecation policy gives roughly two minor-release-cycles' notice (~1 year), but minor releases do remove APIs. The highest-friction migrations of the 3.x line:

3.5 → 3.6

  • matplotlib.cm.get_cmap() deprecated in favor of matplotlib.colormaps[name] / .get_cmap(name) as a method on the ColormapRegistry.
  • mpl.rcsetup.cycler private path removed; use cycler.cycler directly.

3.6 → 3.7

  • Axes3D no longer auto-registers; explicit from mpl_toolkits.mplot3d import Axes3D is harmless but unnecessary — passing projection="3d" is enough.
  • tight_layout warnings turned into hard errors when colorbar layout conflicts.

3.7 → 3.8

  • Python 3.8 dropped.
  • seaborn-* styles re-named to seaborn-v0_8-* to track the upstream seaborn 0.13 visual change.

3.8 → 3.9

  • Several deprecated Axes.tick_params aliases removed.
  • _get_xlim_axes and other underscored helpers removed (they had been quietly used by some plotting libraries).

3.9 → 3.10 (and onward)

  • Python floor moves to 3.10. Animation API tightening around cache_frame_data and save_count continues — explicit kwargs are now required where defaults previously inferred them.

When jumping more than one minor version, the safest path is to upgrade one minor at a time with DeprecationWarning -> error (PYTHONWARNINGS=error::DeprecationWarning) in CI for one release cycle to surface every callsite.

Testing & CI integration

Matplotlib figures are notoriously hard to assert on because rasterisation depends on the platform's freetype build, font cache, and Agg version. The canonical solution is pytest-mpl, which compares baseline PNGs with per-pixel tolerance.

bash
pip install pytest-mpl
pytest --mpl --mpl-baseline-path=tests/baseline

Output: test functions decorated with @pytest.mark.mpl_image_compare are compared against tests/baseline/<name>.png. Mismatches produce a side-by-side diff PNG.

python
import matplotlib.pyplot as plt
import pytest

@pytest.mark.mpl_image_compare(tolerance=20)   # SSIM tolerance
def test_basic_line():
    fig, ax = plt.subplots()
    ax.plot([0, 1, 2], [0, 1, 4])
    return fig

Output: first run with --mpl-generate-path=tests/baseline records baselines; subsequent runs assert against them.

For non-image assertions (axis labels, line counts), use direct introspection:

python
fig, ax = plt.subplots()
ax.plot([0, 1, 2], [0, 1, 4], label="series")
assert ax.get_xlabel() == ""
assert len(ax.lines) == 1
assert ax.lines[0].get_label() == "series"
plt.close(fig)

Output: assertions pass without needing image comparison — best for unit tests that don't care about pixel layout.

CI checklist:

  • Force the backend: MPLBACKEND=Agg in the workflow environment.
  • Pre-warm the font cache as a Docker build step.
  • Pin freetype (the OS package) — pixel diffs vary across freetype 2.10/2.11/2.12.
  • Save failing figures as workflow artifacts so reviewers can eyeball diffs.

Troubleshooting common errors

RuntimeError: Invalid DISPLAY variable / Could not connect to display

The configured backend is trying to open a window on a host with no display. Force Agg before importing pyplot:

bash
MPLBACKEND=Agg python script.py

Output: matplotlib renders headlessly; plt.show() is a no-op.

Format 'jpg' is not supported

Pillow is not installed. pip install pillow and rerun.

UserWarning: Matplotlib is currently using agg, which is a non-GUI backend

You called plt.show() after matplotlib.use("Agg"). Either remove the use() call (and let auto-detect pick a GUI backend) or skip plt.show() and use fig.savefig() instead.

TypeError: Object of type ndarray is not JSON serializable (with seaborn or plotly bridges)

A bridge is trying to serialise a numpy array. Cast to a list: arr.tolist(). Specific to interop with libraries that expect plain Python types.

Animation saves a corrupted MP4 or "writer ffmpeg unavailable"

matplotlib.animation uses an external writer. Install ffmpeg: brew install ffmpeg / apt install ffmpeg. The writer is detected at save-time; if missing, matplotlib falls back to a PIL writer (GIF only) or raises.

Font glyphs render as boxes

The chosen font doesn't have the required glyphs (commonly when plotting CJK or emoji). Set a font that does:

python
plt.rcParams["font.family"] = "Noto Sans CJK SC"

Output: Chinese/Japanese/Korean characters render; you must have the font installed at the OS level.

Compatibility matrix

matplotlibPythonNumPyNotes
3.103.10+1.23+Recent stable; modern Animation API
3.93.9+1.23+Last to support Python 3.9
3.83.9+1.22+seaborn-v0_8-* style names
3.73.8+1.22+tight_layout colorbar fixes
3.63.8+1.21+colormaps registry redesign

Backend × OS matrix (informal — official compatibility list is in the docs):

BackendLinuxmacOSWindowsNotes
AggyesyesyesHeadless; always available
TkAggyesyesyesStdlib tkinter
Qt5Aggyesyesyesneeds pyqt5
Qt6Aggyesyesyesneeds pyqt6
MacOSXyesNative Cocoa; macOS only
WebAggyesyesyesBrowser-based; remote-friendly
nbagg / ipymplyesyesyesJupyter only

Ecosystem integrations

matplotlib is rarely used alone; it's the back end for several higher-level packages and pairs with the rest of the scientific-Python stack.

  • seaborn — statistical visualization on top of matplotlib. import seaborn as sns; sns.set_theme() applies a consistent style and adds plot functions (sns.boxplot, sns.histplot) that return matplotlib axes — drop down to ax. for fine-tuning.
  • pandas .plot() — every DataFrame and Series has .plot.line(), .plot.bar(), .plot.hist(), etc., all rendering through matplotlib. Returns an Axes so the OO API still works after.
  • ipympl — install pip install ipympl, use %matplotlib widget in Jupyter for an interactive figure with zoom/pan inside the notebook output.
  • mplfinance — candlestick / OHLC charts for financial data; thin wrapper that adds finance-specific layouts.
  • cartopy — geographic projections; replaces the deprecated basemap. Works alongside matplotlib, not a fork.
  • mpl-axes-aligner, mpl-interactions, mpld3 — community add-ons for axis alignment, slider-driven plots, and D3 export, respectively.
  • plotly — separate ecosystem, but plotly.tools.mpl_to_plotly(fig) (limited) and the inverse via plotly-matplotlib give a one-way bridge.
  • matplotlib-backend-kitty / -iterm2 — render plots inline in terminals that support graphics protocols (Kitty, iTerm2's inline-images, sixel).

When NOT to use this

  • Truly interactive web charts — for tooltips, hover, zoom, pan baked into the output HTML, use plotly or bokeh. matplotlib's interactive backends require a Python process to be running.
  • Real-time streaming dashboardsbokeh server, dash, or streamlit are purpose-built. matplotlib's animation is fine for offline videos but not for browser streaming.
  • Declarative grammar of graphics — if you think in terms of "encode column X to position, column Y to color", altair or plotnine (ggplot for Python) will feel more natural.
  • 3-D scientific visualization with rotation, lighting, large meshes — use pyvista, mayavi, or vispy. matplotlib's mplot3d is for figure-grade 3-D, not interactive exploration.
  • GUI applications — embedding matplotlib in PyQt/Tk is supported but limited; for a real desktop chart widget consider pyqtgraph.

See also