Written and maintained by CASRAI Editorial Board
Last updated
A contour plot in Matplotlib draws lines of equal value across a two-dimensional grid: contour draws the lines and contourf fills the regions between them. Most disappointing results come from four things the basic call does not handle for you: which of the two functions to use, how to label the lines, what happens to missing data, and how the levels are chosen. This guide covers those first, then the steps. People also search for this as “matplotlib contour”; it is the same function.
What a contour plot is used for in research
A contour plot shows a quantity that depends on two variables, such as a fitted surface, a probability density over two parameters, a loss surface, or a measurement taken on a spatial grid. Lines of constant value make the shape of the surface readable where a scatter plot cannot. For a grid shown as colored cells instead, see heatmap in Matplotlib.
Install or access
Contour plotting is part of Matplotlib; no separate package is needed. The official installation page gives these commands:
python -m pip install -U pip
python -m pip install -U matplotlib
With conda, the same page lists conda install -c conda-forge matplotlib (conda-forge channel) and conda install matplotlib (main channel). If pip tries to build from source and fails, the page suggests adding --prefer-binary. The examples below also use NumPy; see import numpy as np and import matplotlib.pyplot as plt for the standard import conventions.
Step-by-step: draw a labeled contour plot
The steps follow the official Matplotlib contour demo.
- Build the grid. Create the coordinate vectors. X and Y may be either both 2D with the same shape as Z (the form
np.meshgridproduces, used below) or both 1-D, with len(X) equal to the number of columns of Z and len(Y) equal to the number of rows; either way they must be monotonically ordered. Compute Z from them:import numpy as np import matplotlib.pyplot as plt x = np.arange(-3.0, 3.0, 0.025) y = np.arange(-2.0, 2.0, 0.025) X, Y = np.meshgrid(x, y) Z1 = np.exp(-X**2 - Y**2) Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2) Z = (Z1 - Z2) * 2 - Draw the lines. Call
contouron an Axes with X, Y and Z, then label the lines withclabel:fig, ax = plt.subplots() CS = ax.contour(X, Y, Z) ax.clabel(CS, fontsize=10) plt.show()The signature is
contour([X, Y,] Z, /, [levels], **kwargs), and the documentation states that X, Y and Z are positional-only arguments, so they cannot be passed by keyword. - Choose levels yourself. Pass either a number of levels (the demo uses
ax.contour(X, Y, Z, 6, colors='k')) or an explicit list. See the levels section below. - Add a color scale if you fill the regions. Use
contourfandfig.colorbar, as shown in the next section. For colorbar options see colorbar in Matplotlib.
contour vs contourf
Use contour for lines and contourf for filled bands. The documentation says that they “draw contour lines and filled contours, respectively” and that, except as noted, their function signatures and return values are the same, so you can switch between them by changing one word.
The one place the choice changes your code is labeling. clabel labels the lines of a contour set, so a filled plot with line labels needs a second, line-drawing call on top of the filled one. The official contourf demo does exactly this: it draws the filled plot, overlays lines on every second level, and attaches those lines to the colorbar:
fig, ax = plt.subplots(layout='constrained')
CS = ax.contourf(X, Y, Z, 10, cmap="bone")
CS2 = ax.contour(CS, levels=CS.levels[::2], colors='r')
cbar = fig.colorbar(CS)
cbar.add_lines(CS2)
plt.show()
Labels with clabel
ax.clabel(CS, fontsize=10) writes the level value on each line. Three options from the demo cover most needs:
- Inline control. The
inlineargument controls whether the labels are drawn over the line segments, removing the lines beneath each label. - Manual placement. Labels can be placed manually by giving a list of positions in data coordinates:
ax.clabel(CS, fontsize=10, manual=[(-1, -1.4), ...]). - Only some levels. Pass the levels to label and a format string, for example to label every second level:
ax.clabel(CS, levels[1::2], fmt='%1.1f', fontsize=14)
Choosing levels
The official contourf demo warns that automatic selection of contour levels “is usually not such a good idea, because they don’t occur on nice boundaries.” For anything you will publish, pass explicit levels so the bands and the colorbar fall on round values. The demo gives an explicit list and per-band colors:
levels = [-1.5, -1, -0.5, 0, 0.5, 1]
CS3 = ax.contourf(X, Y, Z, levels,
colors=('r', 'g', 'b'),
extend='both')
CS3.cmap.set_under('yellow')
CS3.cmap.set_over('cyan')
The extend argument decides what happens to data outside the listed levels. The demo illustrates all four settings: 'neither', 'both', 'min' and 'max'. With extend='both', values below and above the range are drawn with the colormap’s under and over colors, which you set with set_under and set_over.
Masked and NaN data
Missing values need care. The documentation notes that contourf converts NaN values to masked, and that contouring simply excludes masked or NaN regions: it draws nothing at all in them rather than using the colormap’s “bad” color. Therefore cmap.set_bad("red") would have no effect. If you need missing regions to be visible, mark them another way, for example with a separate patch or annotation on the same Axes.
Output and how to read it
- Close lines mean steep change. Lines are drawn at constant values of Z, so where they bunch together Z changes quickly, and where they are far apart it changes slowly.
- Read the labels, not the colors. With labeled lines or a colorbar that has the lines added, you can read levels directly; with automatic levels the values may be awkward numbers, which is why explicit levels are recommended above.
- Dashed lines are negative. In a single-color plot, negative contours default to dashed. To draw them solid instead, set the rc parameter:
CS = ax.contour(X, Y, Z, 6, colors='k') # negative contours default to dashed plt.rcParams['contour.negative_linestyle'] = 'solid'
For choosing a colormap that stays readable for color-vision differences, see designing effective scientific figures.
Troubleshooting
| Symptom | What to check |
|---|---|
| Error about the shapes of X, Y and Z | The documentation says X and Y must both be 2D with the same shape as Z, or both 1-D with len(X) equal to the number of columns of Z and len(Y) equal to the number of rows of Z. They must also be monotonically ordered. np.meshgrid produces the 2D form. |
TypeError when passing X= or Z= as keywords |
X, Y and Z are positional-only; pass them by position. |
| No labels on a filled plot | clabel labels the lines of a contour set. Overlay a contour call and label that set. |
| Blank area where data is missing | Contouring excludes masked and NaN regions and set_bad does not apply to them. |
| Dashed lines you did not ask for | Negative contours in a single-color plot are dashed by default; set contour.negative_linestyle to 'solid'. |
| Bands not on round values | Pass explicit levels instead of relying on automatic selection. |
Free alternatives
Within Matplotlib itself, the options below are all free and part of the same library; the choice depends on whether you want lines, fills, or both.
| Approach | What it draws | Source |
|---|---|---|
contour |
Contour lines only | pyplot.contour documentation |
contourf |
Filled contours, same signature as contour |
pyplot.contour documentation |
contourf plus contour overlay |
Filled bands with labeled lines on chosen levels, and lines added to the colorbar | contourf demo |
FAQ
What is the difference between contour and contourf in Matplotlib?
contour draws contour lines and contourf draws filled contours. Their signatures and return values are the same except where the documentation notes otherwise.
How do I label contour lines?
Call ax.clabel(CS) on the object returned by contour. Use fontsize, fmt, inline and manual to control appearance and position; for a filled plot, overlay a contour call and label that.
How do I set the contour levels?
Pass a number or an explicit list after Z, for example levels = [-1.5, -1, -0.5, 0, 0.5, 1]. The official demo advises against automatic levels because they do not fall on round boundaries.
Why are some contour lines dashed?
In a single-color plot, negative contours are dashed by default. Set plt.rcParams['contour.negative_linestyle'] = 'solid' to change this.
Sources
- Matplotlib documentation, matplotlib.pyplot.contour (version 3.11.2), checked 2026-10-29.
- Matplotlib gallery, Contour Demo, checked 2026-10-29.
- Matplotlib gallery, Contourf Demo, checked 2026-10-29.
- Matplotlib documentation, Installation, checked 2026-10-29.








