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 e