fill_between shades the area between two curves in Matplotlib. Give it the x values and one y series and it fills down to zero; give it two and it fills the gap between them.
ax.fill_between(x, y) # curve down to zero
ax.fill_between(x, lower, upper, alpha=0.3) # a band between two curves
The two arguments that cause trouble are where=, for shading only part of the range, and interpolate=True, which you almost always want alongside it.
Plots and output below come from Matplotlib 3.11.2 on Python 3.12.5.
Filling between a curve and zero
With a single y series the second boundary defaults to zero:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 200)
y = np.sin(x)
fig, ax = plt.subplots(figsize=(6, 3))
ax.plot(x, y)
ax.fill_between(x, y) # y2 defaults to 0
collection = ax.collections[0]
print("filled between the curve and:", collection.get_paths()[0].vertices[0][1])
print("patches added:", len(ax.collections))
plt.close(fig)
Output:
filled between the curve and: 0.0
patches added: 1
where= with and without interpolate=True.fill_between adds a collection rather than a line, which is why it sits behind your plotted curve by default and why ax.collections is where it turns up.
Filling between two curves
Pass both boundaries and Matplotlib fills the space between them. This is the shape of every error band and forecast range:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 200)
upper = np.sin(x) + 0.4
lower = np.sin(x) - 0.4
fig, ax = plt.subplots(figsize=(6, 3))
ax.plot(x, np.sin(x), color="navy")
ax.fill_between(x, lower, upper, alpha=0.3, color="navy")
print("band width at x=0:", round(upper[0] - lower[0], 2))
print("alpha used :", ax.collections[0].get_alpha())
plt.close(fig)
Output:
band width at x=0: 0.8
alpha used : 0.3
alpha is what keeps your curve readable through the shading. Values between 0.2 and 0.4 work for a single band, and you’ll want lower when bands overlap.
fill_between where= for part of the range
where= takes a boolean array and fills only where it’s true. The classic use is colouring gains and losses differently:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 40) # a coarse grid makes the gap obvious
y = np.sin(x)
fig, ax = plt.subplots(figsize=(6, 3))
ax.plot(x, y, color="black", linewidth=1)
# without interpolate the fill stops at the last grid point, not at the crossing
ax.fill_between(x, y, 0, where=(y > 0), color="green", alpha=0.4)
ax.fill_between(x, y, 0, where=(y < 0), color="red", alpha=0.4,
interpolate=True)
print("points above zero:", int((y > 0).sum()))
print("points below zero:", int((y < 0).sum()))
print("collections drawn:", len(ax.collections))
plt.close(fig)
Output:
points above zero: 24
points below zero: 15
collections drawn: 2
Now look at the middle panel of the figure above. Without interpolate=True the shading stops at the last grid point before the crossing, leaving a white notch at every zero.
interpolate=True works out where the curves actually cross and fills right up to it. On a coarse x grid the difference is obvious; on a fine one it’s subtle but still there.
Reach for it whenever where= involves a comparison between two changing series, not just a fixed threshold.
Confidence bands with fill_between
This is what most people came for: a mean line with a shaded spread around it.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(0)
x = np.arange(0, 30)
runs = rng.normal(loc=np.linspace(10, 25, 30), scale=2.0, size=(50, 30))
mean = runs.mean(axis=0)
sd = runs.std(axis=0)
fig, ax = plt.subplots(figsize=(6, 3))
ax.plot(x, mean, color="#0b6bcb", label="mean")
ax.fill_between(x, mean - sd, mean + sd, alpha=0.25, color="#0b6bcb",
label="±1 sd")
ax.legend()
print("mean at x=0 :", round(mean[0], 2))
print("band at x=0 :", round(mean[0] - sd[0], 2), "to", round(mean[0] + sd[0], 2))
plt.close(fig)
Output:
mean at x=0 : 9.76
band at x=0 : 7.72 to 11.8
Draw the wider band first, or give it a lower alpha, otherwise you won’t see the inner one at all.
The same pattern covers percentile ranges and prediction intervals. Only the arithmetic behind the boundaries changes, so you can reuse the plotting code as-is.
fill_betweenx and flat bands
fill_betweenx is the same idea rotated: you pass y and two x boundaries. For a plain horizontal stripe there’s a simpler option:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
y = np.linspace(0, 10, 100)
fig, ax = plt.subplots(figsize=(5, 3))
ax.plot(np.sin(y), y)
ax.fill_betweenx(y, np.sin(y), 0, alpha=0.3) # x1 and x2, not y1 and y2
# a flat horizontal band, which is what most people want axhspan for
ax.axhspan(4, 6, color="orange", alpha=0.2)
print("fill_betweenx collections:", len(ax.collections))
print("axhspan added a patch :", len(ax.patches))
plt.close(fig)
Output:
fill_betweenx collections: 1
axhspan added a patch : 1
| Want | Use |
|---|---|
| Area between two curves | ax.fill_between(x, y1, y2) |
| The same, horizontally | ax.fill_betweenx(y, x1, x2) |
| A flat horizontal band | ax.axhspan(low, high) |
| A flat vertical band | ax.axvspan(left, right) |
| Only part of the range | add where= and interpolate=True |
axhspan and axvspan span the whole axes regardless of the data, so they’re better for marking a threshold or a date range than anything tied to tick positions.
If you are styling the rest of the figure, these help too:
- Matplotlib set_xticks
- Matplotlib tick_params
- Log scale minor ticks
- Change the background colour
- Matplotlib tight_layout
Frequently asked questions
What does fill_between do in Matplotlib?
It shades the area between two y series across the same x values, or between one series and zero. The arguments are in the fill_between reference.
How do I shade only part of the plot?
Pass a boolean array to where=. Add interpolate=True so the fill reaches the actual crossing point rather than the nearest data point.
Why are there gaps in my fill_between shading?
Because where= without interpolate=True stops at grid points. The gaps appear wherever the condition changes between two samples.
How do I make the fill transparent?
Use alpha, typically 0.2 to 0.4. Lower it further when two bands overlap.
How do I draw a confidence band?
Plot the mean, then ax.fill_between(x, mean - sd, mean + sd, alpha=0.3). Add a second call for a wider interval.
What is the difference between fill_between and fill_betweenx?
fill_between takes x and two y boundaries; fill_betweenx takes y and two x boundaries. Use the second for horizontally oriented plots.
How do I shade a fixed horizontal band?
ax.axhspan(low, high) spans the full width of the axes, which is simpler than building boundary arrays.
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn