skip to content

matplotlib — Foundational Python Plotting Library

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

13 min read 17 snippets deep dive

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#

pip install matplotlib

Output: (none — exits 0 on success)

uv add matplotlib

Output: resolved + added to pyproject.toml

poetry add matplotlib

Output: updated lockfile + virtualenv install

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.

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:

# 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:

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#

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#

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:

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:

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:

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.

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.

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:

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:

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:

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#