Matplotlib Subplots — Complete Guide
Learn to create single and multi-panel figures with Matplotlib subplots: plt.subplots(), GridSpec, shared axes, sizing, and saving examples.
Subplots let you place multiple charts inside a single Matplotlib figure. Instead of creating separate windows for each plot, you arrange them in a grid — side by side, stacked, or in a custom layout — so the reader can compare data at a glance. This chapter covers everything from a simple two-panel figure to flexible grid layouts with shared axes and per-cell spanning.
Before starting, install Matplotlib if you have not already:
pip install matplotlib numpyIf you are new to the library, read the Matplotlib Introduction and Getting Started chapters first.
The Quickest Way: plt.subplots()
plt.subplots(nrows, ncols) is the standard entry point. It returns a Figure and an array of Axes objects.
import matplotlib.pyplot as plt
import numpy as np
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 4))
x = np.linspace(0, 2 * np.pi, 200)
axes[0].plot(x, np.sin(x), color='steelblue')
axes[0].set_title('Sine')
axes[0].set_xlabel('x')
axes[0].set_ylabel('sin(x)')
axes[1].plot(x, np.cos(x), color='darkorange')
axes[1].set_title('Cosine')
axes[1].set_xlabel('x')
axes[1].set_ylabel('cos(x)')
plt.tight_layout()
plt.show()Key points:
figsize=(width, height)sets the figure size in inches. The default is(6.4, 4.8), which is often too small for multiple panels.plt.tight_layout()automatically adjusts spacing so titles and labels do not overlap. Call it right beforeshow()orsavefig().- When
ncols=1andnrows=1(the default),axesis a singleAxesobject, not an array.
Creating a 2×2 Grid
Pass both nrows and ncols to get a 2-D NumPy array of Axes. Index it with [row, col], where both indices are zero-based.
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 2 * np.pi, 200)
fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(10, 8))
# Top-left
ax[0, 0].plot(x, np.sin(x), color='steelblue')
ax[0, 0].set_title('sin(x)')
# Top-right
ax[0, 1].plot(x, np.cos(x), color='darkorange')
ax[0, 1].set_title('cos(x)')
# Bottom-left — clamp tan to avoid ±∞ spikes
y_tan = np.clip(np.tan(x), -10, 10)
ax[1, 0].plot(x, y_tan, color='seagreen')
ax[1, 0].set_title('tan(x) [clipped]')
# Bottom-right
ax[1, 1].plot(x, np.exp(x / np.pi), color='tomato')
ax[1, 1].set_title('exp(x/π)')
plt.suptitle('Trigonometric & Exponential Functions', fontsize=14, y=1.02)
plt.tight_layout()
plt.show()plt.suptitle() adds an overall figure title above all subplots. The y=1.02 nudge pushes it clear of the top-row panel titles.
Iterating Over Axes
For a large grid it is easier to loop over the flattened axes array than to index each cell manually:
import matplotlib.pyplot as plt
import numpy as np
functions = [np.sin, np.cos, np.tan, np.arctan]
names = ['sin', 'cos', 'tan', 'arctan']
colors = ['steelblue', 'darkorange', 'seagreen', 'tomato']
x = np.linspace(-np.pi, np.pi, 300)
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
for ax, fn, name, color in zip(axes.flat, functions, names, colors):
y = fn(x)
y = np.clip(y, -5, 5) # keep tan from dominating the scale
ax.plot(x, y, color=color)
ax.set_title(name)
ax.axhline(0, color='black', linewidth=0.6, linestyle='--')
ax.axvline(0, color='black', linewidth=0.6, linestyle='--')
plt.tight_layout()
plt.show()axes.flat returns a 1-D iterator regardless of the grid shape, so the same loop works for a 2×2, 3×3, or any other layout.
Sharing Axes
When panels represent the same quantity (same x range or same y scale), link their axes so panning or zooming one updates all:
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 4 * np.pi, 400)
# sharex=True links horizontal axes; sharey=True links vertical axes
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 6), sharex=True)
ax1.plot(x, np.sin(x), color='steelblue')
ax1.set_ylabel('sin(x)')
ax1.set_title('Shared x-axis example')
ax2.plot(x, np.sin(2 * x), color='darkorange')
ax2.set_ylabel('sin(2x)')
ax2.set_xlabel('x (radians)')
# Hide redundant x-tick labels on the top panel
ax1.tick_params(labelbottom=False)
plt.tight_layout()
plt.show()sharex=True removes the duplicate x-axis labels from the upper panels and keeps all panels aligned. Use sharey=True similarly when the y scale must match across columns.
Unpacking Axes Directly
When the layout is small and known in advance, you can unpack the returned array directly into named variables:
import matplotlib.pyplot as plt
import numpy as np
# 1 row, 3 columns → unpack into three named axes
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(13, 4))
x = np.linspace(0, 10, 300)
ax1.plot(x, x ** 0.5, color='steelblue', label='√x')
ax1.set_title('Square Root')
ax1.legend()
ax2.plot(x, np.log1p(x), color='darkorange', label='ln(1+x)')
ax2.set_title('Natural Log')
ax2.legend()
ax3.plot(x, x, color='seagreen', label='x')
ax3.set_title('Linear')
ax3.legend()
plt.tight_layout()
plt.show()This is cleaner than axes[0], axes[1], axes[2] for short layouts. For a 2-D grid with two rows, nest the unpacking: (ax1, ax2), (ax3, ax4) = axes.
Custom Layouts with GridSpec
plt.subplots() only creates grids where every cell is the same size. For unequal layouts — a wide overview panel on top with detail panels below — use matplotlib.gridspec.GridSpec:
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np
x = np.linspace(0, 2 * np.pi, 300)
fig = plt.figure(figsize=(10, 8))
gs = gridspec.GridSpec(nrows=2, ncols=3, figure=fig)
# Top row: one wide panel spanning all three columns
ax_top = fig.add_subplot(gs[0, :])
ax_top.plot(x, np.sin(x), color='steelblue', linewidth=2)
ax_top.set_title('Overview — sin(x)')
# Bottom row: three equal panels
ax_bl = fig.add_subplot(gs[1, 0])
ax_bl.plot(x, np.sin(x), color='steelblue')
ax_bl.set_title('sin(x)')
ax_bm = fig.add_subplot(gs[1, 1])
ax_bm.plot(x, np.cos(x), color='darkorange')
ax_bm.set_title('cos(x)')
ax_br = fig.add_subplot(gs[1, 2])
ax_br.plot(x, np.sin(x) * np.cos(x), color='seagreen')
ax_br.set_title('sin(x)·cos(x)')
plt.tight_layout()
plt.show()The gs[row, col] slice syntax mirrors NumPy slicing. gs[0, :] spans all three columns; gs[1, 0] takes only the first cell in row 1.
Controlling Row and Column Heights
GridSpec accepts height_ratios and width_ratios to make some rows or columns larger than others:
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np
fig = plt.figure(figsize=(9, 7))
gs = gridspec.GridSpec(
2, 2,
height_ratios=[3, 1], # top row is 3× taller than bottom row
width_ratios=[2, 1], # left column is 2× wider than right column
hspace=0.4,
wspace=0.3,
)
x = np.linspace(0, 4 * np.pi, 400)
ax1 = fig.add_subplot(gs[0, 0])
ax1.plot(x, np.sin(x), color='steelblue')
ax1.set_title('Main (tall + wide)')
ax2 = fig.add_subplot(gs[0, 1])
ax2.plot(x, np.cos(x), color='darkorange')
ax2.set_title('Side (tall + narrow)')
ax3 = fig.add_subplot(gs[1, 0])
ax3.plot(x, np.sin(x) ** 2, color='seagreen')
ax3.set_title('Bottom-left (short + wide)')
ax4 = fig.add_subplot(gs[1, 1])
ax4.plot(x, np.cos(x) ** 2, color='tomato')
ax4.set_title('Bottom-right (short + narrow)')
plt.suptitle('Proportional Grid Layout', fontsize=13)
plt.show()hspace and wspace control the vertical and horizontal space between subplots (as a fraction of the average subplot height/width).
Setting a Common Figure Size
The figsize parameter always refers to the whole figure, not individual subplots. A good rule of thumb:
| Layout | Recommended figsize |
|---|---|
| 1 row × 2 cols | (10, 4) |
| 1 row × 3 cols | (13, 4) |
| 2 rows × 2 cols | (10, 8) |
| 3 rows × 3 cols | (13, 11) |
You can always adjust based on your data; these are starting points that leave room for titles and labels.
Adding Titles and Labels
Each Axes object has its own title and axis labels. The figure as a whole can have a supertitle:
import matplotlib.pyplot as plt
import numpy as np
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
x = np.linspace(0, 5, 100)
for ax, power, color in zip(axes, [2, 3], ['steelblue', 'darkorange']):
ax.plot(x, x ** power, color=color)
ax.set_title(f'$x^{power}$') # LaTeX in title
ax.set_xlabel('x')
ax.set_ylabel(f'$x^{power}$')
ax.grid(True, linestyle='--', alpha=0.5)
plt.suptitle('Power Functions', fontsize=14)
plt.tight_layout()
plt.show()Saving a Multi-Panel Figure
Call plt.savefig() before plt.show() (calling show() first clears the figure):
import matplotlib.pyplot as plt
import numpy as np
fig, axes = plt.subplots(1, 3, figsize=(13, 4))
x = np.linspace(0, 2 * np.pi, 300)
axes[0].plot(x, np.sin(x), color='steelblue')
axes[0].set_title('sin(x)')
axes[1].plot(x, np.cos(x), color='darkorange')
axes[1].set_title('cos(x)')
axes[2].plot(x, np.sin(x) + np.cos(x), color='seagreen')
axes[2].set_title('sin(x) + cos(x)')
plt.tight_layout()
plt.savefig('trig_panels.png', dpi=150, bbox_inches='tight')
plt.show()bbox_inches='tight' crops out any extra whitespace around the figure — useful when embedding the image in a document or web page.
Common Pitfalls
Forgetting tight_layout()
Without tight_layout(), subplot titles and axis labels from adjacent panels often overlap. Always call it before show() or savefig().
Wrong index dimensions
plt.subplots(2, 3) returns a 2-D array; plt.subplots(1, 3) returns a 1-D array. Trying to use axes[0, 0] on a 1-D array raises an IndexError. Either use axes[0], or pass squeeze=False to always get a 2-D array:
fig, axes = plt.subplots(1, 3, squeeze=False)
# axes is now shape (1, 3) — always index with [row, col]
axes[0, 0].plot(...)
axes[0, 1].plot(...)
axes[0, 2].plot(...)Suptitle clipping
plt.suptitle() can be clipped by tight_layout(). Fix it by passing a rect to tight_layout:
plt.tight_layout(rect=[0, 0, 1, 0.95]) # leave 5% at top for suptitleOr use fig.subplots_adjust(top=0.90) instead.
Quick Reference
| Task | Code |
|---|---|
| 1×2 grid | fig, (ax1, ax2) = plt.subplots(1, 2) |
| 2×2 grid | fig, ax = plt.subplots(2, 2) |
| Access cell | ax[row, col] |
| Iterate all cells | for ax in axes.flat: |
| Share x-axis | plt.subplots(2, 1, sharex=True) |
| Share y-axis | plt.subplots(1, 2, sharey=True) |
| Always 2-D array | plt.subplots(..., squeeze=False) |
| Custom layout | gs = GridSpec(rows, cols); fig.add_subplot(gs[r, c]) |
| Span columns | fig.add_subplot(gs[0, :]) |
| Height ratios | GridSpec(2, 1, height_ratios=[3, 1]) |
| Figure title | plt.suptitle('Title') |
| Save figure | plt.savefig('out.png', dpi=150, bbox_inches='tight') |
Related Chapters
- Matplotlib Introduction — library overview and installation
- Matplotlib Getting Started — your first plot
- Matplotlib Line Plots — plotting continuous data
- Matplotlib Labels — axis labels, titles, and annotations
- Matplotlib Grid — adding and styling grid lines
- Matplotlib Bar Charts — comparing categories
- Matplotlib Scatter Plots — relationships between variables
- Matplotlib Histograms — data distributions