Matplotlib
Matplotlib turns your numbers into pictures. Looking at data before modelling it is the single habit that catches the most mistakes.
- 13 min read
- 3 reading levels
- Published
Read these first
On this page 7
One lesson, three depths. Pick the one that fits you today — you can switch any time.
Beginner — No maths. Plain English.
Matplotlib turns your numbers into pictures — lines, bars, dots and grids you can look at.
When someone is admitted to hospital, a chart hangs at the foot of the bed. A nurse writes the temperature on it every few hours. Nobody reads that chart as a list of readings. They glance at the line and see whether the fever is climbing or breaking.
Your eyes are far quicker than your arithmetic. A chart hands the work to the quick part of you.
Why you should care
Before you train any model, you should look at your data. Not describe it. Look at it.
A picture catches things a summary hides. One sensor stuck at the same reading for a week. A price column where somebody typed the amount in paise instead of rupees. A whole month missing.
Those faults are invisible in a table of averages and glaring in a chart. Plotting first is the cheapest bug-catching habit in the whole field.
Why it exists
In 2003, a researcher named John Hunter was studying brain activity in patients with epilepsy. The tool his lab used for charts was expensive, needed a licence per person, and was awkward to script.
He wanted charts he could produce from Python, on any machine, for free. So he wrote matplotlib. Almost every plot in a Python AI notebook still comes from it. Or from a library sitting on top of it.
How it works
A matplotlib picture is built the way you would build one on paper:
Figure → the sheet of paper you are drawing on
Axes → one chart box on that sheet (a sheet can hold several)
Axis → the numbered lines down the side and along the bottom
The marks → the actual line, bars or dotsYou make the sheet and put one or more chart boxes on it. Then you draw into a box, label it, and save the sheet as an image.
Your numbers → choose a chart type → add labels → save as a pictureChoosing the chart type is the part that matters most:
LINE → something changing over time (rainfall by month)
BAR → comparing separate groups (sales by city)
DOTS → do two things move together? (study hours and marks)
HISTOGRAM→ how are values spread out? (how tall is everyone)A real example you have seen
Open the weather app on your phone and look at the rain forecast for the week. That row of little bars is a chart of predicted rainfall.
You did not read a single number, and you already know whether to carry an umbrella on Thursday. That is the entire point of plotting.
An honest word
Matplotlib is powerful and its naming is inconsistent, because it grew over twenty years and kept old commands working. You will find two ways to do the same thing and no clear signpost about which to use.
Learn one way and stay with it: make a figure and axes explicitly, then call methods on the axes. Many confusing examples online use the other way. Knowing there are two ways will save you an hour of doubt.
Remember this
- Matplotlib draws pictures from numbers, so your eyes can do the work.
- Always look at your data before you model it — plots catch faults that summaries hide.
- Choose the chart type to match the question you are asking.
What to learn next
- Pandas — the tables you will be plotting.
- Python for machine learning — from a clean table to a model.
- Statistics — reading what a chart is really telling you.
Developer — Code and libraries.
Setup
pip install matplotlibA line chart, saved without a screen
import os
import matplotlib
matplotlib.use("Agg") # draw to a file, not to a window - works on any server
import matplotlib.pyplot as plt
months = ["Jun", "Jul", "Aug", "Sep"]
rain_mm = [180, 410, 320, 150]
fig, ax = plt.subplots(figsize=(6, 3.5))
ax.plot(months, rain_mm, marker="o", color="#1f77b4", linewidth=2)
ax.set_title("Monsoon rainfall, Pune")
ax.set_xlabel("Month")
ax.set_ylabel("Rainfall (mm)")
ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig("rainfall.png", dpi=120)
plt.close(fig)
print("backend :", matplotlib.get_backend())
print("saved :", os.path.basename("rainfall.png"))
print("exists :", os.path.exists("rainfall.png"))
print("size :", os.path.getsize("rainfall.png") > 5000, "(bigger than 5 KB)")backend : Agg saved : rainfall.png exists : True size : True (bigger than 5 KB)
The real result is the image file, so the printed lines only confirm it was written. Open rainfall.png and you will see a single blue line with round markers. It rises steeply from June to July, then falls away through September:
mm
410 | o
| / \
320 | / o
| / \
180 | o \
150 | o
+-----+-----+-----+-----
Jun Jul Aug SepThe exact file size varies with your matplotlib version and font rendering. That is why the check above is a threshold rather than a fixed number.
Two panels on one figure
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(seed=0) # fixed seed so your numbers match mine
hours = rng.normal(loc=5.0, scale=1.5, size=200)
marks = 40 + 6 * hours + rng.normal(0, 5, size=200)
fig, (left, right) = plt.subplots(1, 2, figsize=(9, 3.5))
left.hist(hours, bins=15, color="#4c9f70", edgecolor="white")
left.set_title("How many hours students studied")
left.set_xlabel("Hours per day")
left.set_ylabel("Number of students")
right.scatter(hours, marks, s=14, alpha=0.6, color="#c1666b", label="student")
right.set_title("Study hours vs marks")
right.set_xlabel("Hours per day")
right.set_ylabel("Marks")
right.legend(loc="lower right")
fig.tight_layout()
fig.savefig("study.png", dpi=120)
plt.close(fig)
print("hours mean :", round(float(hours.mean()), 2))
print("marks mean :", round(float(marks.mean()), 2))
print("figure size:", fig.get_size_inches(), "inches")
print("saved study.png")hours mean : 5.02 marks mean : 69.69 figure size: [9. 3.5] inches saved study.png
Those two means are reproducible because default_rng(seed=0) fixes the random sequence. Remove the seed and the numbers change on every run. Fixing the seed for teaching and debugging is good practice; reporting a result from a single fixed seed is not.
The saved image holds a bell-shaped histogram on the left, centred near five hours. On the right sits a cloud of dots climbing from lower left to upper right. More hours, higher marks, with real scatter around the trend.
Line-by-line walkthrough
matplotlib.use("Agg") picks the drawing engine before pyplot is imported. Agg renders to an image buffer with no window system. Use it on any server, in Docker, or over SSH. Skip this line on a laptop where you want a pop-up window.
fig, ax = plt.subplots() creates the sheet and one chart box, and hands you both. This is the explicit style. The alternative, calling plt.plot(...) directly, works on a hidden "current figure" and gets confusing the moment you have two charts.
figsize=(6, 3.5) is width and height in inches, not pixels. Pixels come from figsize * dpi. At dpi=120 this file is 720 by 420 pixels.
alpha=0.6 sets opacity. On a scatter plot with many points it reveals where dots pile up, which a solid colour hides.
fig.tight_layout() recomputes spacing so axis labels and titles are not clipped. Without it, long y-axis labels get cut off at the edge.
plt.close(fig) releases the figure. Inside a loop that makes hundreds of charts, skipping this leaks memory. Eventually it triggers a warning about too many open figures.
Common mistakes
1. Calling savefig after show.
plt.show()
plt.savefig("out.png") # writes a blank imageSome backends clear the figure when the window closes. Save first, then show.
2. Expecting a window on a server. On a headless machine you get an error or a silent no-op.
UserWarning: FigureCanvasAgg is non-interactive, and thus cannot be shown
Fix: set the Agg backend and use savefig.
3. A chart with no axis labels.
A line with unlabelled axes is decoration, not information. Nobody can tell millimetres from centimetres. set_xlabel, set_ylabel, set_title cost three lines and make the chart usable by someone other than you.
4. Using a rainbow colour map for values.
cmap="jet" puts sharp visual edges where the data is smooth and hides real steps elsewhere. It also breaks for colour-blind readers. Use cmap="viridis" for ordinary values and cmap="RdBu" when zero is a meaningful centre.
Try it yourself
Add a second line to the rainfall chart for last year's numbers, [150, 380, 355, 210]. Then call ax.legend() so the two lines are distinguishable. Give each ax.plot call a label="2025" style argument, otherwise the legend has nothing to show.
Then swap ax.plot for ax.bar and decide which one reads better. Neither answer is wrong — the line implies continuous change, the bars imply four separate totals.
What to learn next
- Pandas —
df.plot()calls matplotlib for you. - Statistics — distributions, spread, and what a histogram shows.
- Python for machine learning — plotting training curves and errors.
Researcher — Mathematics and papers.
The object model
Matplotlib is a scene graph, not a plotting command list. Everything drawable is an Artist, and Artists form a containment tree:
Figure canvas, dpi, figure-level layout engine
└── Axes one coordinate system, one data area
├── XAxis, YAxis Ticks, Locators, Formatters, spines
├── Line2D a polyline with markers
├── Rectangle one bar, one histogram bin
├── Collection many primitives sharing one draw call
└── Text, Legend, AnnotationRendering walks this tree in zorder order and asks each Artist to draw itself onto a Renderer. Two things follow:
- Every property is inspectable and mutable after creation.
ax.lines[0].set_color("red")works, because theLine2Dstill exists. - Per-Artist overhead is real. Ten thousand separate
Line2Dobjects are far slower than oneLineCollectionof ten thousand segments. The pixels are identical.
The pyplot module is a stateful wrapper that tracks a "current figure" and "current axes" and forwards calls to them. It exists for interactive convenience. Library code should take an ax argument and never touch global state.
The transform pipeline
A point travels through composable affine and non-affine transforms:
data coords --transData--> axes coords --transAxes--> figure coords --dpi_scale_trans--> display pixels- data coords are your numbers, in your units.
- axes coords run
0to1across the data area. This is how you pin an annotation to "the top-right corner", whatever the data range. - figure coords run
0to1across the whole sheet. - display coords are device pixels.
transData is the composition of a non-linear scale transform with an affine map. Setting ax.set_yscale("log") swaps in a logarithmic scale transform; nothing else in the pipeline changes. Custom projections — polar, geographic — are implemented as new transforms rather than as special-cased drawing code.
Physical units are consistent throughout: font sizes and line widths are in points, where 1 point is 1/72 inch. Figure size is in inches. Output pixel dimensions are figsize * dpi. Setting dpi therefore scales the raster resolution without changing the relative size of text to data.
Backends
The backend splits into a renderer and a canvas.
| Backend | Output | Use |
|---|---|---|
Agg | PNG raster, via the Anti-Grain Geometry library | servers, CI, batch generation |
PDF, SVG, PS | vector | publication figures that must scale |
QtAgg, TkAgg, macosx | on-screen window | local interactive work |
module://matplotlib_inline.backend_inline | PNG or SVG into a notebook | Jupyter default |
For a figure with very many primitives, vector output balloons in size. artist.set_rasterized(True) rasterises that one Artist while keeping axes and text as vectors. That is the usual compromise for scatter plots in a PDF.
Colour, treated as a measurement problem
A colour map is a function from a scalar to a colour. If it is to be read quantitatively, equal steps in the data must produce equal perceived steps in colour. Perceptual difference is approximated by distance in a uniform colour space such as CIELAB, using the CIEDE2000 difference metric.
The jet and rainbow maps fail this badly. Lightness is non-monotonic. They create bright bands that readers interpret as features in the data. They also flatten regions that are genuinely different. They also collapse under common forms of colour vision deficiency. Those affect roughly 8 percent of men of northern European descent.
viridis was designed by Stéfan van der Walt and Nathaniel Smith, and became the matplotlib default in version 2.0. It is monotonic in lightness and near-uniform in perceived difference. It also survives deuteranopia simulation. Practical selection:
- Sequential data with one direction of meaning:
viridis,magma,cividis. - Diverging data with a meaningful zero:
RdBu,coolwarm. Setvmin=-vmaxso zero sits at the centre colour. - Categorical labels:
tab10ortab20. Never a continuous map, because a continuous map implies an ordering your categories do not have.
Configuration and reproducibility
matplotlib.rcParams holds every default. A style sheet is a file of rcParams, applied with plt.style.use("path/to/paper.mplstyle") or scoped with with plt.style.context(...). Pin the style file alongside the analysis code. A figure regenerated a year later under different defaults is a silent inconsistency in a paper.
For deterministic raster output across machines, fix figsize, dpi, the style sheet, and the font. Font substitution is the usual cause of figures that differ byte-for-byte between a laptop and CI. svg.fonttype = "none" keeps text as text in SVG, but requires the font on the viewing machine. Setting it to "path" converts glyphs to outlines, which is portable.
References
- Hunter, J. D. "Matplotlib: A 2D Graphics Environment." Computing in Science & Engineering 9(3), 90–95, 2007.
- van der Walt, S., Smith, N. "A Better Default Colormap for Matplotlib." SciPy 2015 talk, and the accompanying
viscmtool. - Borland, D., Taylor, R. M. "Rainbow Color Map (Still) Considered Harmful." IEEE Computer Graphics and Applications 27(2), 14–17, 2007.
- Crameri, F., Shephard, G. E., Heron, P. J. "The misuse of colour in science communication." Nature Communications 11, 5444, 2020.
- Cleveland, W. S., McGill, R. "Graphical Perception: Theory, Experimentation, and Application to the Development of Graphical Methods." Journal of the American Statistical Association 79(387), 531–554, 1984. The ranking of visual encodings that justifies preferring position over area or colour.
- Anscombe, F. J. "Graphs in Statistical Analysis." The American Statistician 27(1), 17–21, 1973. Four datasets with identical summary statistics and entirely different shapes.
What to learn next
- Statistics — the quantities a chart is estimating.
- Pandas — the data structures behind
df.plot(). - Model evaluation — ROC curves, calibration plots, and residual plots.