Ending the Subplot Overlap Struggle with Matplotlib Constrained_Layout
Replace brittle subplots_adjust calls with Matplotlib's constrained_layout engine to automatically prevent label and colorbar overlap in multi-panel publication figures.
31 Jul 2026, 19:42 UTC

When you build a multi-panel figure in Matplotlib, the default spacing often leaves long y-axis labels clipped or colorbars overlapping neighboring axes. The usual fix is a chain of plt.subplots_adjust() calls that hard-codes margins for one figure size. Change the figure size, add a panel, or update a label, and the layout breaks again. The useful takeaway is that Matplotlib ships with a declarative layout engine, constrained_layout, that measures the rectangular bounding boxes of artists during the draw cycle and solves for margins that keep titles, tick labels, legends, and colorbars from colliding.
Why manual subplot tuning is brittle
Manual spacing uses absolute fractions of the figure canvas. A left margin of 0.2 that fits a label at 100 dpi may clip the same label at 300 dpi or when the font size changes. Because the numbers are static, they do not adapt to the actual rendered text. In contrast, constrained_layout measures the artists and solves for the smallest margins that keep them separate. That makes the layout data-driven rather than pixel-driven.
Activating the layout engine
There are three supported ways to turn the engine on. For reproducible scripts, set it at figure creation so the first draw already uses the solver:
- Constructor:
fig = plt.figure(constrained_layout=True) - Setter:
fig.set_constrained_layout(True) - Global default:
plt.rcParams['figure.constrained_layout.use'] = True
You can confirm the active engine with fig.get_layout_engine(). The engine stabilized in Matplotlib 3.0; earlier releases had limited support for some artists such as inset axes and secondary axes. If you support older environments, verify the version with print(matplotlib.__version__) before relying on it.
Worked example: a 2×2 gridspec with per-axis colorbars
The following pattern is common in scientific plotting: four heatmaps, each with its own colorbar and a long y-label. With tight_layout or manual margins, the colorbars often collide with the right-hand labels or get clipped at the figure edge. Enabling constrained_layout removes the manual subplots_adjust calls and keeps spacing stable when data limits or labels change.
import matplotlib.pyplot as plt
import numpy as np
# Run this in a Python environment with Matplotlib 3.0 or newer.
fig, axes = plt.subplots(
2, 2,
figsize=(10, 8),
constrained_layout=True,
)
for i, ax in enumerate(axes.flat):
data = np.random.default_rng(i).random((10, 10))
im = ax.imshow(data, cmap="viridis")
ax.set_ylabel(f"Subplot {i + 1} with a long label name")
ax.set_title(f"Data Analysis {i + 1}")
# Per-axis colorbar; the solver reserves space for it automatically.
fig.colorbar(im, ax=ax, shrink=0.6)
plt.show()
No manual margin values appear in the script. If you later change figsize or lengthen the y-labels, the engine recomputes the layout on the next draw.
Trade-offs and limitations
The engine is not free, and it does not cover every artist.
- Draw-time cost: Layout is recomputed on every draw. Static figures rarely notice the overhead, but animations or interactive sessions with many subplots can feel slower.
- Conflict with manual adjustment: Do not mix
constrained_layoutwithplt.subplots_adjust()orfig.subplots_adjust(). The solver overrides or fights manual values, producing unpredictable results. If you need a fixed margin for one edge, use the engine's pad parameters instead. - Artist support: Most built-in artists report tight bounding boxes correctly, but some custom overlays or third-party extensions do not. If a custom element still overlaps, the engine cannot see it to move other content.
How to verify the result
A practical check is to build two figures with identical content, one with constrained_layout=True and one without, then compare them visually and inspect the engine state:
# Confirm the engine is active and inspect applied padding.
print(fig.get_layout_engine())
print(fig.get_constrained_layout_pads())
# Save at different DPI values and confirm labels remain non-overlapping.
fig.savefig("panel_100dpi.png", dpi=100)
fig.savefig("panel_300dpi.png", dpi=300)
If the pads dictionary returns expected values and the saved images show clean margins at both resolutions, the layout is working as intended. If you see overlap, check for stray subplots_adjust calls and verify that all custom artists expose proper bounding boxes.
Switching to constrained_layout turns subplot spacing from a manual chore into a declarative property of the figure. For publication pipelines that must survive changes in size, DPI, and label length, that shift is usually worth the extra draw-time cost.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.