PYTHON / MATPLOTLIB
Subplots and shared axes
Build multi-panel Matplotlib figures with plt.subplots, link panels using sharex/sharey, and remove the redundant inner tick labels.
What you will learn
- Unpack plt.subplots correctly for 1x1, 1xN, and NxM grids without index errors
- Choose between sharex/sharey values True, 'row', 'col', and False deliberately
- Hide inner tick labels with label_outer() instead of deleting ticks
- Label a whole grid once using fig.supxlabel and fig.supylabel
Understanding Subplots and shared axes
plt.subplots(nrows, ncols) does two things in one call: it creates a Figure and it fills a grid of Axes into it, returning the figure and a NumPy array of Axes. The shape of that array depends on the grid, and it is squeezed by default: a 2x2 grid gives shape (2, 2) so you index axes[row, col], a 1x3 grid gives shape (3,) so you index axes[i], and a 1x1 grid gives a bare Axes object with no indexing at all. Passing squeeze=False forces a 2-D array in every case, which is what you want in a function that must handle any grid size.
Sharing is not a drawing option, it is a structural link. When you pass sharex=True, Matplotlib registers every Axes in one group and makes each x Axis reuse the same view interval, the same locator and the same formatter as its siblings. That is why set_xlim on one panel silently changes all of them, why autoscaling picks a range that fits the data of the whole group rather than one panel, and why calling set_xticks on a single shared panel retics every panel in the group. The payoff is honest comparison: two panels on a shared scale cannot mislead you about relative size the way two independently autoscaled panels can.
Because siblings show identical ticks, the inner ones are pure noise. plt.subplots already suppresses them for shared axes, and ax.label_outer() does the same job for axes you built by hand: it keeps the x tick labels only on the bottom row and the y tick labels only on the first column. Note that it turns labels off with tick_params(labelbottom=False), which is per-Axes state, rather than touching the shared locator. Since each panel then has no axis label of its own, describe the quantity once for the whole figure with fig.supxlabel and fig.supylabel.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 2, figsize=(8, 5), sharex=True, sharey=True,
layout="constrained")
print("axes container:", type(axes).__name__, axes.shape)
xs = [0, 1, 2, 3, 4]
for k, ax in enumerate(axes.flat, start=1):
ax.plot(xs, [x ** k for x in xs])
ax.set_title(f"x ** {k}")
axes[0, 0].set_xlim(0, 6)
print("bottom-right xlim:", axes[1, 1].get_xlim())
axes[1, 1].set_ylim(0, 300)
print("top-left ylim:", axes[0, 0].get_ylim())
print("top-left shows x labels:",
axes[0, 0].xaxis.get_tick_params()["labelbottom"])
print("bottom-left shows x labels:",
axes[1, 0].xaxis.get_tick_params()["labelbottom"])
fig.supxlabel("x")
fig.supylabel("y")
fig.savefig("powers.png", dpi=100)
print("wrote powers.png")Shared axes make several Axes reuse one view interval and one tick locator, so limits, autoscaling and tick positions become properties of the group rather than of a single panel.
Worked examples
Sharing by row and by column
Shows that sharex='col' and sharey='row' create separate groups, so a limit change spreads only inside its own group.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 2, sharex="col", sharey="row")
axes[0, 0].set_xlim(0, 1)
axes[0, 1].set_xlim(0, 100)
print("column 0 bottom xlim:", axes[1, 0].get_xlim())
print("column 1 bottom xlim:", axes[1, 1].get_xlim())
axes[0, 0].set_ylim(-1, 1)
print("row 0 right ylim:", axes[0, 1].get_ylim())
print("row 1 right ylim:", axes[1, 1].get_ylim())
group = axes[0, 0].get_shared_x_axes().get_siblings(axes[0, 0])
print("x group size:", len(group))
plt.close(fig)Example explained
Line 1sharex='col' puts the two axes of each column in one x group, so the two columns keep independent x ranges.
Line 2set_ylim(-1, 1) on the top-left panel reaches the top-right panel because sharey='row' groups them, but leaves the bottom row untouched at its default (0.0, 1.0).
Line 3get_shared_x_axes().get_siblings(ax) reports the actual group membership, which is the quickest way to check that sharing is what you think it is.
Line 4The group size is 2, not 4, confirming that 'col' created two groups instead of one.
Manual sharing on an uneven grid
Links a tall price panel to a short volume panel with add_subplot(..., sharex=...) and cleans up the duplicated axis with label_outer().
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(6, 6), layout="constrained")
top = fig.add_subplot(3, 1, (1, 2))
bottom = fig.add_subplot(3, 1, 3, sharex=top)
days = [1, 2, 3, 4, 5, 6, 7]
top.plot(days, [10, 11, 13, 12, 15, 14, 16])
top.set_ylabel("price")
bottom.bar(days, [100, 140, 90, 200, 160, 120, 180])
bottom.set_ylabel("volume")
bottom.set_xlabel("day")
top.label_outer()
print("top keeps x labels:",
top.xaxis.get_tick_params()["labelbottom"])
print("bottom is a sibling:",
bottom in top.get_shared_x_axes().get_siblings(top))
top.set_xlim(1, 7)
print("bottom xlim:", bottom.get_xlim())
plt.close(fig)Example explained
Line 1add_subplot(3, 1, (1, 2)) spans the first two cells of a 3x1 grid, so the price panel is twice as tall as the volume panel.
Line 2sharex=top links the second Axes to the first at creation time; there is no supported way to add that link afterwards.
Line 3top.label_outer() switches the upper panel's bottom tick labels off because it is not in the last row; it does that with tick_params(labelbottom=False), which is the flag the print reads back.
Line 4Zooming with top.set_xlim(1, 7) moves the bar panel too, so the two views can never drift out of alignment.
Important notes
Sharing is fixed when the Axes are created: there is no public way to unshare later, so build the figure again if you change your mind.
layout='constrained' and fig.supxlabel/fig.supylabel need Matplotlib 3.5 and 3.4 respectively; on older versions use fig.tight_layout() and put labels on the outer panels.
ax.get_xticklabels() returns an empty list once labels are switched off, so all(t.get_visible() for t in ...) is vacuously True and cannot tell you whether labels are showing. Read ax.xaxis.get_tick_params()['labelbottom'] instead.
Common mistakes
Indexing the return value as if it were always 2-D: plt.subplots(1, 3) squeezes to shape (3,), so axes[0, 1] raises IndexError, and plt.subplots(1, 1) returns a bare Axes, so axes[0] raises TypeError about a non-subscriptable object.
Hiding duplicated ticks with axes[0].set_xticks([]) on a shared axis; because siblings share one locator, every panel in the group loses its ticks, including the bottom one you wanted to keep. Use label_outer() or tick_params(labelbottom=False).
Turning on sharey for series with very different magnitudes, for example a count near 10 next to a count near 100000; autoscaling fits the group, so the small series is drawn as a flat line on the baseline and looks like missing data.
Try it yourself
Change, predict, then run
Create a 3x1 grid with sharex=True showing temperature, humidity and pressure against the same hour values, then print the x limits of all three panels after calling set_xlim(0, 23) on the middle one only to confirm the link.
Open the Python workspaceCheck your understanding
You create fig, axes = plt.subplots(3, 1, sharex=True), plot into all three panels, then call axes[0].set_xticks([0, 5, 10]) hoping to change only the top panel. What actually happens?
- Only the top panel gets those ticks; the other two keep their automatic ticks
- All three panels get ticks at 0, 5 and 10, because shared axes reuse the same tick locator
- Matplotlib raises an error because tick positions cannot be set on a shared axis
- The tick positions change on all three panels but the printed tick labels stay as they were
Show answer
Sharing makes each x Axis reuse its sibling's locator and formatter, so a fixed locator installed from one panel governs the whole group. The first option is the tempting one because most Axes methods are local, but tick positions are exactly the state that sharing makes global; only label visibility stays per-panel, which is why label_outer() works and set_xticks does not.