Using Matplotlib's constrained_layout for Reliable Subplot Spacing
Learn how Matplotlib's constrained_layout solver automatically adjusts subplot spacing for labels, colorbars and legends, and when to prefer it over tight_layout.
23 Dec 2025, 09:28 UTC

The spacing problem in Matplotlib figures
When you add long axis labels, a shared colorbar, or a legend to a multi‑axes figure, the default layout often clips text or leaves uneven gaps. Calling tight_layout() helps, but it works after the figure is drawn and may still need manual rect or pad tweaks for colorbars and legends.
How constrained_layout solves the problem
constrained_layout activates a constraint solver (the layoutgrid module) that treats every axis, colorbar, legend, tick label and suptitle as a variable. The solver finds a set of left, right, bottom and top positions that satisfy all spacing constraints simultaneously, before the first draw.
Enabling the solver
You can turn it on globally with plt.rcParams['figure.constrained_layout.use'] = True or per figure by passing constrained_layout=True to plt.figure() or Figure(). The active state can be queried with fig.get_constrained_layout().
constrained_layout versus tight_layout
tight_layout – a sequential heuristic
tight_layout() runs after the figure has been rendered. It measures the actual pixel extents of labels, titles and other artists, then adjusts subplot positions to minimize white space. Because it is a post‑draw pass, artists added after the initial render (e.g., a colorbar created later) often need explicit pad or rect arguments to avoid being clipped.
constrained_layout – a simultaneous solver
The solver builds a linear system that includes the size requirements of colorbars created with ax.colorbar() and legends from ax.legend(). Consequently, those elements receive appropriate padding without extra code. Starting with Matplotlib 3.5, nested GridSpec objects and Figure.subfigures are also handled correctly, making constrained_layout the preferred choice for complex publication‑ready layouts.
Performance wise, the solver adds a small overhead per draw. For interactive canvases with many axes (≈50+), the extra step can be noticeable, and in those cases disabling the solver and using manual subplots_adjust may be faster. For typical figures with a few dozen subplots the difference is imperceptible.
Worked example: migrating a GridSpec figure with a shared colorbar
The following script shows a before‑and‑after comparison. The first figure uses the classic tight_layout() approach with manual rect tuning; the second figure enables constrained_layout=True and removes the manual adjustments.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np
# Sample data
x = np.linspace(0, 2*np.pi, 400)
y = np.sin(x**2)
# ---- Figure 1: tight_layout with manual rect ----
fig1 = plt.figure(constrained_layout=False)
gs1 = gridspec.GridSpec(2, 2, figure=fig1, wspace=0.3, hspace=0.3)
axs1 = []
for i in range(2):
row = []
for j in range(2):
ax = fig1.add_subplot(gs1[i, j])
ax.plot(x, y)
ax.set_ylabel(f'Very long y‑axis label {i}{j} that may wrap')
row.append(ax)
axs1.append(row)
# Shared colorbar placed below the grid
cax1 = fig1.add_axes([0.2, 0.05, 0.6, 0.03])
norm = plt.Normalize(v=-1, v=1)
sm = plt.cm.ScalarMappable(cmap='viridis', norm=norm)
sm.set_array([])
cbar1 = fig1.colorbar(sm, cax=cax1, orientation='horizontal')
cbar1.set_label('Intensity')
# tight_layout needs a rect to make room for the colorbar
fig1.tight_layout(rect=[0, 0.1, 1, 0.95])
# ---- Figure 2: constrained_layout ----
fig2 = plt.figure(constrained_layout=True)
gs2 = gridspec.GridSpec(2, 2, figure=fig2)
axs2 = []
for i in range(2):
row = []
for j in range(2):
ax = fig2.add_subplot(gs2[i, j])
ax.plot(x, y)
ax.set_ylabel(f'Very long y‑axis label {i}{j} that may wrap')
row.append(ax)
axs2.append(row)
cax2 = fig2.add_axes([0.2, 0.05, 0.6, 0.03])
cbar2 = fig2.colorbar(sm, cax=cax2, orientation='horizontal')
cbar2.set_label('Intensity')
# No manual tight_layout or rect needed
# Verification prints
print('Figure 1 constrained_layout?', fig1.get_constrained_layout())
print('Figure 2 constrained_layout?', fig2.get_constrained_layout())
print('Layout engine keys (fig2):', list(fig2.get_layout_engine().get().keys()))
Running the script produces two figures side by side. In the first figure you will notice that the colorbar either overlaps the bottom axes or leaves a large gap unless you fine‑tune the rect values. In the second figure the solver automatically allocates space for the long y‑labels and the horizontal colorbar, so nothing is clipped and the spacing looks balanced.
Limitations and practical checks
- Do not mix
constrained_layoutwith manualsubplots_adjustor a call totight_layout()on the same figure; the results are undefined. - Annotations placed with
xycoords='figure fraction'are not part of the constraint system and can still overlap axes. Test such annotations carefully or fall back to manual positioning. - When one element is much larger than the others (e.g., a huge colorbar beside many tiny axes) the solver may introduce generous gaps. You can fine‑tune the padding via the
constrained_layout_padsrcParam (available since Matplotlib 3.6). - Some third‑party backends (certain WebAgg configurations) have historically had incomplete support; verify the output matches expectations for your target backend.
To confirm that the solver is active and to inspect its internal state, run the following after creating a figure:
import matplotlib
print('Matplotlib version:', matplotlib.__version__)
fig = plt.gcf()
print('Constrained layout active:', fig.get_constrained_layout())
engine_state = fig.get_layout_engine().get()
print('Engine variables (sample):', {k: engine_state[k] for k in list(engine_state.keys())[:5]})
The engine state dictionary contains the solver’s variables for each axis edge; unexpected values can guide you to adjust constrained_layout_pads or revert to manual layout for that specific figure.
Actionable takeaway
Start new multi‑axes figures with Figure(constrained_layout=True) or enable the rcParam globally. Verify the layout with the two‑line check above, and only reach for tight_layout() or manual subplots_adjust if the solver produces gaps that conflict with your design. By letting the constraint solver handle spacing for labels, colorbars and legends, you spend less time tweaking rect tuples and more time on the actual data.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.