Fixing Overlapping Labels and Clipped Titles in Matplotlib Subplots
Overlapping tick labels and clipped titles in Matplotlib are layout problems, not drawing bugs. Diagnose the active layout engine, then apply constrained_layout or tight_layout correctly.
23 Dec 2025, 04:16 UTC

The recognizable condition
You build a figure with several subplots, and the result looks wrong in a familiar way: tick labels from one Axes collide with the neighboring plot, the suptitle sits on top of the top-row titles, an axis label is sliced off at the figure edge, or a colorbar squeezes everything into a corner. It often appears only after you add something — a legend placed outside the Axes, long tick labels, a second row of plots — to a figure that previously looked fine.
The useful takeaway: this is almost never a drawing bug. It is a layout problem, and Matplotlib ships two layout engines — constrained_layout and tight_layout — that exist specifically to solve it. The job is to confirm which engine (if any) is active, enable the right one, and know the cases where neither works.
Why the default layout fails
By default, Matplotlib places each Axes at a fixed rectangle within the figure. Those rectangles are computed without knowing how large your text will be, because text extents are only measurable at draw time (they depend on the font, the renderer, and the DPI). So the default spacing parameters — left, right, hspace, wspace — are guesses that match average content. Long tick labels, rotated labels, colorbars, and outside legends all exceed those guesses, and the overflow either overlaps a neighbor or falls off the figure edge.
Cause and diagnostic table
| Symptom | Likely cause | First check |
|---|---|---|
| Labels overlap or clip in a plain subplot grid | No layout engine active | fig.get_layout_engine() returns None |
| Layout fixed on screen but clipped in the saved file | Saved without bbox_inches='tight', or DPI/size mismatch | Compare on-screen figure size to saved image dimensions |
| Spacing correct once, broken after adding artists | tight_layout() called before all artists existed | Re-call after the last artist is added |
| Unpredictable spacing plus a warning about both engines | tight_layout and constrained_layout both enabled | Check rcParams and explicit calls; pick one |
| Layout engine active but one Axes still misplaced | Manually placed Axes via fig.add_axes(), inset, or 3D Axes | Engine skips or mishandles these; use manual spacing |
Ordered checks
1. Confirm which layout engine is active
Run this in the same script or notebook session that creates the figure (Matplotlib 3.6 or newer; on older versions this method does not exist):
import matplotlib.pyplot as plt
fig, axs = plt.subplots(2, 2)
print(fig.get_layout_engine())
A None result means nothing is managing spacing, which fully explains the overlap. No special permissions are needed; this is a read-only inspection and changes no state.
2. Check whether a style sheet changed the default
Styles and config files can silently alter layout behavior:
print(plt.rcParams['figure.constrained_layout'])
If this prints True and you did not set it, a style sheet or your matplotlibrc did — useful to know before you start passing conflicting arguments.
3. Reproduce minimally
Build a 2x2 grid with deliberately long tick labels and a suptitle, once with the default layout and once with an engine enabled. Comparing the two side by side confirms the engine is the fix and not something else in your plotting code.
Fixes tied to the findings
Fix 1: enable constrained_layout at figure creation
This is the preferred remedy for standard subplot grids. The engine measures text extents at draw time and reserves room automatically, and it re-runs on every draw, so later additions are accommodated:
fig, axs = plt.subplots(2, 2, constrained_layout=True, figsize=(8, 6))
for ax in axs.flat:
ax.plot([0, 1], [0, 1])
ax.set_xticklabels(['a very long tick label', 'another long one'],
rotation=45)
fig.suptitle('Figure title that needs room')
Alternatively set it globally with plt.rcParams['figure.constrained_layout'] = True. Expected result: labels, titles, and colorbars get reserved space; nothing overlaps or clips. If you still see clipping after this, move to the escalation section rather than stacking more fixes.
Fix 2: use tight_layout when constrained_layout is not an option
If your figure relies on manual GridSpec positioning that constrained_layout handles poorly, call tight_layout() once, after every artist has been added:
fig.tight_layout()
Two differences matter. First, it runs once — add a colorbar or new title afterward and you must call it again. Second, it is mutually exclusive with constrained_layout: enabling both produces warnings and unpredictable spacing, so remove one before relying on the other.
Fix 3: check the saved output, not just the screen
A figure can look correct on screen and still clip in the file. savefig honors the figure's declared size unless you pass bbox_inches='tight', which crops to the drawn artists:
fig.savefig('out.png') # declared figure bounds
fig.savefig('out_tight.png', bbox_inches='tight') # cropped to artists
Verify by comparing the pixel dimensions of the two files. Be aware that bbox_inches='tight' produces output sizes that differ from the declared figure size, which can surprise automated pipelines that expect fixed dimensions — pick one convention and use it consistently.
Escalation criteria
If a layout engine is active and content still overlaps or clips, the problem is no longer standard layout. Check, in order:
- Artists outside layout management. Text added with
fig.text()in figure coordinates, or artists withclip_on=False, are not accounted for by either engine. Reposition them manually or convert to Axes-coordinates text. - Manually placed Axes. Axes created with
fig.add_axes(), some inset Axes, and certain 3D Axes have historically interacted poorly with constrained_layout. For these, fall back tofig.subplots_adjust()or an explicitGridSpecwith hand-set spacing. - Version-sensitive behavior. GridSpec support in constrained_layout improved across releases and differs meaningfully before Matplotlib 3.2. Check
matplotlib.__version__before assuming engine behavior matches current documentation; upgrade if you are on an old release.
If none of these apply, the remaining step is to reduce the figure to the smallest script that still shows the problem — that minimal case is what you want before filing a bug or asking for help, since layout issues are otherwise hard to describe.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.