Stop Hand-Tuning Subplot Margins: Let Matplotlib's tight_layout() Do the Arithmetic
Hand-tuning subplot margins in Matplotlib is a guess-and-check loop you can skip. tight_layout() measures your labels and computes spacing automatically — here's how to use it, tune it, and recognize its limits.
14 Nov 2025, 05:08 UTC

You've built a four-panel figure, and the y-axis label of the left column is sitting on top of the tick labels of the right column. The title of the top row is clipped by the figure edge. So you open the docs, start nudging plt.subplots_adjust(left=0.12, bottom=0.15, ...), re-run, nudge again, and ten minutes later the figure looks acceptable — until you change the font size and everything overlaps again.
The takeaway: for most grid-style figures, you can skip that loop entirely. Matplotlib ships with tight_layout(), which measures the actual rendered size of your labels, titles, and tick marks and computes subplot spacing to fit them. One call replaces the guess-and-check cycle.
What tight_layout() actually does
When Matplotlib draws a figure, axis labels and titles live outside the axes rectangle, in figure space. The default subplot parameters reserve fixed fractions of the figure for margins, and those fractions know nothing about how long your labels are. A y-label like "Temperature (°C)" needs more room than "x".
tight_layout() runs after the artists exist, asks each one how much space it occupies, and adjusts the subplot parameters (left, right, top, bottom, wspace, hspace) so nothing collides. Because it's based on measured extents rather than fixed fractions, it adapts when you change fonts, tick label lengths, or the number of panels.
A worked example
Run this in any Python environment with Matplotlib installed (no special permissions needed; it just opens a window or writes a file):
import matplotlib
print(matplotlib.__version__) # confirm you're on a recent 3.x release
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 200)
fig, axs = plt.subplots(2, 2, figsize=(8, 6))
for i, ax in enumerate(axs.flat):
ax.plot(x, np.sin(x + i))
ax.set_title(f"Panel {chr(65 + i)}: phase shift {i}")
ax.set_xlabel("Time (seconds)")
ax.set_ylabel("Amplitude (arbitrary units)")
# Comment this line out and re-run to see the overlap it fixes.
fig.tight_layout()
fig.savefig("panels.png", dpi=150)
plt.show()With the tight_layout() line commented out, the long y-labels and titles crowd into neighboring panels. With it enabled, each panel gets exactly the margin its labels need. To verify the difference yourself, save both versions and compare panels.png side by side — the check is purely visual, which is appropriate for a layout feature.
Note the call is fig.tight_layout() on the figure object. The pyplot shortcut plt.tight_layout() does the same thing for the current figure; use whichever matches how you structured your code.
Tuning the padding when the default isn't quite right
Three parameters cover most adjustments:
pad— overall padding between the figure edge and subplots, as a fraction of font size. Default is 1.08; increase it if a long title still feels cramped.h_padandw_pad— vertical and horizontal padding between adjacent subplots.rect— a bounding box(left, bottom, right, top)in figure coordinates that constrains the layout. This is the standard way to reserve space for a figure-level suptitle:fig.tight_layout(rect=[0, 0, 1, 0.96])leaves the top 4% forfig.suptitle(...).
One ordering rule matters: call tight_layout() after all plotting and labeling is done. It measures what exists at call time, so labels added afterward won't be accounted for. If you're saving with savefig, an alternative worth knowing is fig.savefig("out.png", bbox_inches="tight"), which crops the saved file to the drawn content — useful, but it changes the output dimensions, which can matter for publication templates.
Where tight_layout() falls short
It's a heuristic, not a general constraint solver, and a few cases expose that:
- 3-D axes and some artist types don't report their extents accurately, so spacing can be wrong. Expect manual adjustment there.
- Very wide or tall figures can end up with excessive padding, because padding scales with font size rather than figure dimensions.
- Complex grids with mixed panel sizes (via
GridSpecwith spans) sometimes confuse it; Matplotlib's newerconstrained_layoutengine (plt.subplots(layout="constrained")) handles those cases more robustly and is the better default for new, complicated figures. - Animations need care — recalculating layout every frame causes visible jitter, so fix the layout once instead.
Also, if you're pinned to a Matplotlib version older than 3.3, be aware that tight_layout had known miscalculations with shared axes; upgrading is the practical fix. Check your version with the one-liner in the example above.
The practical default
For a straightforward grid of 2-D subplots, make fig.tight_layout() the last line before saving or showing. Reach for rect when you add a suptitle, and switch to constrained_layout when panels have unequal sizes or you need colorbars to align cleanly. Reserve manual subplots_adjust for the cases — 3-D, animations, pixel-exact templates — where the automatic tools genuinely can't know what you want.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.