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
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.xseries; 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.xminor releases may remove APIs that were deprecated in two prior releases (typically ~1 year notice). - A future
4.0has 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 stdlibtkinter. Works out of the box on most desktop installs. - QtAgg / Qt5Agg / Qt6Agg — needs
pip install pyqt5orpyqt6for 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 ipympland 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 whentext.usetexis enabled for LaTeX-rendered labels.
Alternatives
| Package | Trade-off |
|---|---|
seaborn | Statistical wrapper on matplotlib. Use for quick categorical + distribution plots; falls back to matplotlib for customization. |
plotly | Interactive browser charts (WebGL). Use when readers click and zoom; bundle size is much larger. |
bokeh | Interactive browser charts with a Python-server option. Use for streaming dashboards. |
altair | Declarative grammar of graphics over vega-lite. Use when you prefer "describe the chart" over "draw the chart". |
pygal | SVG-only output, simple API. Use for lightweight reports where SVG embedding matters more than feature breadth. |
Common gotchas
- Backend chaos.
plt.show()behaviour depends entirely on the configured backend, which is auto-selected frommatplotlibrc, theMPLBACKENDenv var, and what is importable. On macOS theMacOSXbackend conflicts withTkAggif both are present. Set explicitly:import matplotlib; matplotlib.use("Agg")beforeimport matplotlib.pyplot. - Defaults are baked at import.
rcParamsreads are evaluated when modules import; settingplt.rcParams[...]after creating a figure usually has no effect for that figure. Set them at the top of the script or via amatplotlibrcfile. %matplotlib inlinevswidgetin Jupyter.inlineproduces static PNGs;widgetneedsipymplinstalled and a JupyterLab extension enabled. Switching mid-notebook leaves stale display handles — restart the kernel after changing.- Mixing with seaborn / plotly resets the style.
import seaborncallsseaborn.set_theme()as a side-effect on older versions; it overwrites yourrcParams. Pin the import order or callsns.reset_defaults(). - Animation API churn.
matplotlib.animation.FuncAnimationquietly changed argument semantics across3.xminor releases (especially aroundcache_frame_dataandblit). Pin matplotlib if you have CI-stable animations. - Memory leaks from
plt.figure()withoutplt.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 callplt.close(fig)when done. - 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.mp4 — ffmpeg 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 ofmatplotlib.colormaps[name]/.get_cmap(name)as a method on theColormapRegistry.mpl.rcsetup.cyclerprivate path removed; usecycler.cyclerdirectly.
3.6 → 3.7
Axes3Dno longer auto-registers; explicitfrom mpl_toolkits.mplot3d import Axes3Dis harmless but unnecessary — passingprojection="3d"is enough.tight_layoutwarnings turned into hard errors when colorbar layout conflicts.
3.7 → 3.8
- Python 3.8 dropped.
seaborn-*styles re-named toseaborn-v0_8-*to track the upstream seaborn 0.13 visual change.
3.8 → 3.9
- Several deprecated
Axes.tick_paramsaliases removed. _get_xlim_axesand 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_dataandsave_countcontinues — 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=Aggin 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
| matplotlib | Python | NumPy | Notes |
|---|---|---|---|
| 3.10 | 3.10+ | 1.23+ | Recent stable; modern Animation API |
| 3.9 | 3.9+ | 1.23+ | Last to support Python 3.9 |
| 3.8 | 3.9+ | 1.22+ | seaborn-v0_8-* style names |
| 3.7 | 3.8+ | 1.22+ | tight_layout colorbar fixes |
| 3.6 | 3.8+ | 1.21+ | colormaps registry redesign |
Backend × OS matrix (informal — official compatibility list is in the docs):
| Backend | Linux | macOS | Windows | Notes |
|---|---|---|---|---|
| Agg | yes | yes | yes | Headless; always available |
| TkAgg | yes | yes | yes | Stdlib tkinter |
| Qt5Agg | yes | yes | yes | needs pyqt5 |
| Qt6Agg | yes | yes | yes | needs pyqt6 |
| MacOSX | — | yes | — | Native Cocoa; macOS only |
| WebAgg | yes | yes | yes | Browser-based; remote-friendly |
| nbagg / ipympl | yes | yes | yes | Jupyter 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 toax.for fine-tuning. - pandas
.plot()— every DataFrame and Series has.plot.line(),.plot.bar(),.plot.hist(), etc., all rendering through matplotlib. Returns anAxesso the OO API still works after. - ipympl — install
pip install ipympl, use%matplotlib widgetin 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 viaplotly-matplotlibgive 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
plotlyorbokeh. matplotlib's interactive backends require a Python process to be running. - Real-time streaming dashboards —
bokehserver,dash, orstreamlitare 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",
altairorplotnine(ggplot for Python) will feel more natural. - 3-D scientific visualization with rotation, lighting, large meshes — use
pyvista,mayavi, orvispy. matplotlib'smplot3dis 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
- Python: matplotlib — API tutorial, pyplot vs OO, common chart types
- Packages: pip-pillow — required for JPEG/WebP output
- Packages: pip-jupyter — the common host for matplotlib output