Matplotlib tight_layout(): Fix Overlapping Subplots and Labels

plt.tight_layout() automatically adjusts the spacing around and between subplots so that titles, axis labels and tick labels do not overlap or get cut off. Call it once after you have added all titles and labels and before plt.show() or savefig(). This Matplotlib tight_layout guide shows what it changes, its pad, w_pad, h_pad and rect parameters, how it differs from bbox_inches='tight', and what to do when you see Tight layout not applied.

Tested with Python 3.12.5 and Matplotlib 3.11.2. The plots below are real Matplotlib windows and the console output is from the Windows Command Prompt. The official reference is the matplotlib.pyplot.tight_layout documentation and the tight layout guide.

What does plt.tight_layout() do?

By default Matplotlib uses fixed margins and fixed gaps between subplots. They do not grow when you add titles or long axis labels, so neighbouring subplots collide. Here is a 2×2 grid without tight_layout():

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)
fig, axes = plt.subplots(2, 2, figsize=(7, 5))
for i, ax in enumerate(axes.flat, start=1):
    ax.plot(x, np.sin(x * i))
    ax.set_title(f"Sensor {i}: vibration signal")
    ax.set_xlabel("Time (seconds)")
    ax.set_ylabel("Amplitude (mm)")

plt.show()
Matplotlib window with a 2x2 grid of subplots whose titles and x-axis labels overlap because tight_layout was not called
Without tight_layout(): titles run into the x-axis labels of the row above.

Add one line before plt.show():

plt.tight_layout()     # recompute the spacing so nothing overlaps
plt.show()
The same Matplotlib 2x2 subplot grid after plt.tight_layout(), with all titles and axis labels readable
With plt.tight_layout(): the gaps grow until every label fits.

tight_layout() measures the tick labels, axis labels and titles of every subplot and then changes the figure’s subplot parameters (left, right, bottom, top, wspace, hspace). You can print them to see exactly what it changed:

import matplotlib.pyplot as plt

fig, axes = plt.subplots(2, 2, figsize=(7, 5))
for ax in axes.flat:
    ax.set_title("Monthly revenue")
    ax.set_xlabel("Month")
    ax.set_ylabel("USD (thousands)")

def spacing(fig):
    p = fig.subplotpars
    return {k: round(float(getattr(p, k)), 3) for k in ("left", "right", "bottom", "top", "wspace", "hspace")}

print("before:", spacing(fig))
fig.tight_layout()
print("after: ", spacing(fig))

Output:

before: {'left': 0.125, 'right': 0.9, 'bottom': 0.11, 'top': 0.88, 'wspace': 0.2, 'hspace': 0.2}
after:  {'left': 0.095, 'right': 0.963, 'bottom': 0.117, 'top': 0.927, 'wspace': 0.293, 'hspace': 0.491}
Command Prompt output showing fig.subplotpars left, right, bottom, top, wspace and hspace before and after fig.tight_layout()
The subplot parameters before and after tight_layout().

It is a one-time adjustment: if you add a title or label after calling it, call it again. Resizing the window also does not re-run it unless you use figure.autolayout or a layout engine, shown below.

tight_layout() parameters: pad, w_pad, h_pad and rect

ParameterDefaultWhat it controls
pad1.08Space between the figure edge and the subplots, as a fraction of the font size
w_padsame as padExtra width between columns of subplots
h_padsame as padExtra height between rows of subplots
rect(0, 0, 1, 1)The box (left, bottom, right, top) in figure coordinates that the subplots must fit in

More space with pad, w_pad and h_pad

plt.tight_layout(pad=2.0, w_pad=3.0, h_pad=2.5)   # more room: around the figure, between columns, between rows
Matplotlib subplot grid spaced with tight_layout pad=2.0, w_pad=3.0 and h_pad=2.5, showing wider gaps between plots
Larger pad, w_pad and h_pad values give the grid more breathing room.

Leave room for a suptitle or legend with rect

tight_layout() only arranges the subplots. A fig.suptitle() or a figure-level legend is not taken into account, so they often end up on top of the plots. Pass rect to keep the subplots inside a smaller box and leave that space free:

import matplotlib.pyplot as plt
import numpy as np

x = np.arange(1, 13)
fig, axes = plt.subplots(1, 3, figsize=(9, 3.8), sharey=True)
for ax, (region, growth) in zip(axes, {"East": 1.2, "Central": 0.9, "West": 1.5}.items()):
    ax.plot(x, 100 + growth * x ** 1.5, label="Sales")
    ax.plot(x, 90 + x * 4, "--", label="Target")
    ax.set_title(region)
    ax.set_xlabel("Month")
axes[0].set_ylabel("Units")

fig.suptitle("2025 sales by region", fontsize=14, fontweight="bold")
fig.legend(["Sales", "Target"], loc="lower center", ncols=2)

# rect=(left, bottom, right, top) in figure coordinates: keep the subplots inside this box,
# leaving 10% at the bottom for the legend and 8% at the top for the suptitle
fig.tight_layout(rect=(0, 0.10, 1, 0.92))
plt.show()
Matplotlib figure with three regional sales subplots, a bold suptitle at the top and a shared legend at the bottom kept clear with tight_layout rect
rect=(0, 0.10, 1, 0.92) keeps 10% free at the bottom for the legend and 8% at the top for the title.

fig.tight_layout() vs plt.tight_layout()

They do the same thing. plt.tight_layout() works on the current figure; fig.tight_layout() works on the figure you name, which is clearer when you have several figures. There is no ax.tight_layout(), because layout is a property of the whole figure:

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.tight_layout()      # wrong object: tight_layout() belongs to the Figure

Result:

Traceback (most recent call last):
  File "C:\pyguides\ax_tight_layout_error.py", line 4, in <module>
    ax.tight_layout()      # wrong object: tight_layout() belongs to the Figure
    ^^^^^^^^^^^^^^^
AttributeError: 'Axes' object has no attribute 'tight_layout'. Did you mean: '_in_layout'?
Command Prompt traceback AttributeError: 'Axes' object has no attribute 'tight_layout' after calling ax.tight_layout()
ax.tight_layout() raises AttributeError; use fig.tight_layout().

If you only have an axes, get its figure: ax.figure.tight_layout().

tight_layout() vs savefig(bbox_inches=’tight’)

They are often confused, but they solve different problems:

  • tight_layout() moves the subplots inside the figure so they do not overlap. The figure size stays the same.
  • savefig(..., bbox_inches='tight') does not move anything. It changes the size of the saved image so that everything drawn fits, trimming empty margins or growing to include text that sticks out.
import matplotlib.pyplot as plt
from PIL import Image

fig, ax = plt.subplots(figsize=(4, 3))
ax.plot([1, 2, 3], [10, 30, 20])
ax.set_title("Monthly sales", fontsize=14)

fig.savefig("normal.png", dpi=100)
fig.savefig("tight_bbox.png", dpi=100, bbox_inches="tight")   # trim the PNG to what is drawn

for name in ("normal.png", "tight_bbox.png"):
    print(name, Image.open(name).size)

Output:

normal.png (400, 300)
tight_bbox.png (357, 297)
Command Prompt output comparing the pixel size of a PNG saved normally and one saved with bbox_inches='tight'
The same figure saved twice: bbox_inches='tight' produced a different image size.

For saved charts, use both: fig.tight_layout() to fix overlaps between subplots, then fig.savefig('chart.png', bbox_inches='tight') to trim the outer whitespace. Add pad_inches=0.1 to control the margin that is kept.

UserWarning: Tight layout not applied

When the labels need more room than the figure has, tight_layout() gives up and leaves the layout unchanged with a warning:

import matplotlib.pyplot as plt

fig, axes = plt.subplots(4, 4, figsize=(3, 3))     # 16 subplots squeezed into a 3 x 3 inch figure
for ax in axes.flat:
    ax.set_title("Quarterly result", fontsize=10)
    ax.set_xlabel("Quarter")
    ax.set_ylabel("Value")
fig.tight_layout()
print("figure size:", fig.get_size_inches())

Result:

C:\pyguides\tight_layout_warning.py:8: UserWarning: Tight layout not applied. tight_layout cannot make Axes height small enough to accommodate all Axes decorations.
  fig.tight_layout()
figure size: [3. 3.]
Command Prompt showing UserWarning: Tight layout not applied because 16 subplots with labels do not fit in a 3 by 3 inch figure
Sixteen labelled subplots cannot fit in a 3×3 inch figure.

Fixes, from most to least effective:

  • Make the figure bigger, for example figsize=(10, 8).
  • Remove repeated labels: use sharex=True / sharey=True and label only the outer subplots with ax.label_outer(), or use fig.supxlabel() and fig.supylabel().
  • Use smaller fonts for titles and tick labels.
  • Set the spacing yourself with plt.subplots_adjust(left=..., bottom=..., wspace=..., hspace=...). This is also the practical choice for very large grids (for example 50 subplots): tight_layout() is a heuristic, so with many subplots or very uneven label sizes it can fail or give uneven gaps, while subplots_adjust() is fixed and predictable.

Apply tight layout to every figure automatically

Set figure.autolayout once at the top of a script or in your matplotlibrc file, and every figure is laid out tightly each time it is drawn, including after the window is resized:

import matplotlib.pyplot as plt

plt.rcParams["figure.autolayout"] = True     # every new figure calls tight_layout() when it is drawn

fig, axes = plt.subplots(1, 2, figsize=(6, 3))
print("layout engine:", type(fig.get_layout_engine()).__name__)

Output:

layout engine: TightLayoutEngine

tight_layout vs constrained layout

Matplotlib also has a newer layout engine, constrained layout. You turn it on when you create the figure, and it keeps adjusting while you work. It also makes room for colorbars, suptitles and figure legends, which tight_layout() ignores:

import matplotlib.pyplot as plt
import numpy as np

fig, axes = plt.subplots(2, 2, figsize=(7, 5), layout="constrained")   # no tight_layout() call needed
for i, ax in enumerate(axes.flat, start=1):
    im = ax.imshow(np.random.default_rng(i).random((10, 10)), cmap="viridis")
    ax.set_title(f"Heat map {i}")
fig.colorbar(im, ax=axes, shrink=0.8, label="Value")
fig.suptitle("Constrained layout handles colorbars and suptitles")
plt.show()
Matplotlib figure using layout='constrained' with four heat maps, a shared colorbar and a suptitle, all spaced automatically
layout="constrained" spaces subplots, the colorbar and the suptitle.
tight_layout()layout=”constrained”
When it runsOnce, when you call it (or on every draw with figure.autolayout)On every draw
Suptitle, figure legend, colorbarNot handled; use rectHandled
Combine with subplots_adjust()YesNo (it is ignored)
Best forSimple grids, quick fixes, old codeNew code, colorbars, complex layouts

For a detailed side-by-side comparison, see Matplotlib constrained_layout vs tight_layout.

Keep going with these Matplotlib layout tutorials:

Frequently asked questions

What does plt.tight_layout() do?

It recalculates the subplot margins and the gaps between subplots so that titles, axis labels and tick labels fit without overlapping. It changes the figure’s subplot parameters; the figure size stays the same.

Where should I call plt.tight_layout()?

After all titles, labels and ticks are set and before plt.show() or savefig(). If you add labels later, call it again.

What is the rect parameter in tight_layout?

A tuple (left, bottom, right, top) in figure coordinates (0 to 1). The subplots are fitted inside that box, which leaves room outside it for a suptitle or a figure legend. The default is (0, 0, 1, 1).

What is the difference between tight_layout and bbox_inches=’tight’?

tight_layout() rearranges the subplots inside the figure. bbox_inches='tight' only changes the size of the saved image so everything drawn fits. Use both for saved charts.

Why do I get UserWarning: Tight layout not applied?

The labels need more space than the figure has, so Matplotlib keeps the old layout. Use a larger figsize, share axes and remove repeated labels, use smaller fonts, or set spacing with subplots_adjust().

Should I use tight_layout or constrained layout?

For new code, layout="constrained" is usually better because it handles colorbars, suptitles and legends and updates on every draw. tight_layout() is fine for simple subplot grids.