Stop Hand-Tuning Spacing: Matplotlib's constrained_layout in Practice
Matplotlib's constrained_layout engine ends the trial-and-error of tight_layout and subplots_adjust. Here's how it works, a 2x2 example, and when not to use it.
20 May 2026, 21:34 UTC

Every matplotlib user knows the ritual: you build a figure, save it, open the PNG, and discover the y-axis labels are sliced off or two subplot titles are overlapping. So you add a tight_layout() call, or worse, start nudging subplots_adjust(left=0.13, bottom=0.11, ...) by trial and error until it looks right — at one figure size, on one screen.
There is a better default hiding in plain sight. Matplotlib ships an opt-in layout engine called constrained_layout that solves this class of problem automatically, and for most multi-axes figures it removes spacing code entirely.
What constrained_layout actually does
Instead of you telling matplotlib where the axes should sit, constrained_layout measures the space each Axes' decorations need — tick labels, axis labels, titles, the figure suptitle — and resizes the axes so nothing overlaps and nothing gets clipped. You enable it when creating the figure:
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 2, layout='constrained')
# or: fig = plt.figure(layout='constrained')
# or globally: plt.rcParams['figure.constrained_layout.use'] = TrueThe key difference from tight_layout() is when the work happens. tight_layout() is a one-shot function: it adjusts positions once, at the moment you call it. constrained_layout participates in every draw. Resize the window interactively, save at a different DPI, or change the figure size, and the layout recomputes itself. You never re-call anything.
A worked example: 2×2 time series with long labels
Say you are plotting four sensor channels with descriptive y-labels and a shared colorbar. Run this in any Python environment with matplotlib installed (check yours with python -c "import matplotlib; print(matplotlib.__version__)" — the layout='constrained' keyword has been supported for several major releases):
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(0)
t = np.linspace(0, 10, 500)
fig, axes = plt.subplots(2, 2, figsize=(8, 5), layout='constrained')
for i, ax in enumerate(axes.flat):
im = ax.plot(t, np.sin(t + i) + 0.1 * rng.standard_normal(t.size))
ax.set_title(f'Channel {i}')
ax.set_ylabel('Amplitude (microvolts, uncorrected)')
fig.suptitle('Sensor readout')
fig.savefig('sensors.png', dpi=150)Save the same figure without layout='constrained' and compare the two PNGs: the default layout typically clips the long left-hand y-labels, while the constrained version fits them with zero manual spacing arguments. A practical check: save at two different figsize values and confirm the labels stay intact in both — no re-calling a layout function required.
GridSpec, ratios, and colorbars
The engine is GridSpec-aware. Width and height ratios are respected while overlap prevention still applies:
fig = plt.figure(layout='constrained')
gs = fig.add_gridspec(2, 2, width_ratios=[2, 1], height_ratios=[1, 1])
ax_main = fig.add_subplot(gs[:, 0])
ax_top = fig.add_subplot(gs[0, 1])
ax_bot = fig.add_subplot(gs[1, 1])Colorbars created with fig.colorbar(mappable, ax=ax) are handled sensibly: the engine steals space from the associated axes so the colorbar stays aligned with its parent. This is the case that used to produce the ugliest manual hacks.
Trade-offs and limitations
Three things to know before switching everything over:
- It is mutually exclusive with
tight_layout(). Pick one per figure; mixing them produces warnings or surprising results. - It costs draw time. The solver runs on every draw, so fast interactive animations or frequently redrawn figures can feel sluggish. For those, compute the layout once with
tight_layoutor fixed margins instead. - Manually placed axes are ignored. Anything added with
fig.add_axes([...])sits outside the layout engine, and deeply nested layouts (subfigures with their own colorbars) may still need manual tuning. Behavior has improved across releases, so verify on your installed version.
The takeaway
If your figure is a conventional grid of subplots — with or without shared colorbars — start with layout='constrained' and delete your spacing code. Reach for tight_layout() only when draw performance matters, and for subplots_adjust only when you need pixel-level manual control. The quickest way to convince yourself: take an existing figure with clipped labels, add one keyword argument, and re-save.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.