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 enough to accommodate all Axes decorations.
fig.tight_layout()
figure size: [3. 3.]
Fixes, from most to least effective:
- Make the figure bigger, for example
figsize=(10, 8). - Remove repeated labels: use
sharex=True/sharey=Trueand label only the outer subplots withax.label_outer(), or usefig.supxlabel()andfig.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, whilesubplots_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()
layout="constrained" spaces subplots, the colorbar and the suptitle.| tight_layout() | layout=”constrained” | |
|---|---|---|
| When it runs | Once, when you call it (or on every draw with figure.autolayout) | On every draw |
| Suptitle, figure legend, colorbar | Not handled; use rect | Handled |
| Combine with subplots_adjust() | Yes | No (it is ignored) |
| Best for | Simple grids, quick fixes, old code | New 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:
- Constrained layout vs tight layout in Matplotlib
- Adjust subplot spacing with subplots_adjust()
- Create multiple plots in one figure
- Put the legend outside the plot
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.
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn