W3docs

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 numpy

If 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 before show() or savefig().
  • When ncols=1 and nrows=1 (the default), axes is a single Axes object, 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:

LayoutRecommended 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 suptitle

Or use fig.subplots_adjust(top=0.90) instead.

Quick Reference

TaskCode
1×2 gridfig, (ax1, ax2) = plt.subplots(1, 2)
2×2 gridfig, ax = plt.subplots(2, 2)
Access cellax[row, col]
Iterate all cellsfor ax in axes.flat:
Share x-axisplt.subplots(2, 1, sharex=True)
Share y-axisplt.subplots(1, 2, sharey=True)
Always 2-D arrayplt.subplots(..., squeeze=False)
Custom layoutgs = GridSpec(rows, cols); fig.add_subplot(gs[r, c])
Span columnsfig.add_subplot(gs[0, :])
Height ratiosGridSpec(2, 1, height_ratios=[3, 1])
Figure titleplt.suptitle('Title')
Save figureplt.savefig('out.png', dpi=150, bbox_inches='tight')
Was this page helpful?