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()

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()

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}

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
| Parameter | Default | What it controls |
|---|---|---|
pad | 1.08 | Space between the figure edge and the subplots, as a fraction of the font size |
w_pad | same as pad | Extra width between columns of subplots |
h_pad | same as pad | Extra 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

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()

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'?

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)

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