Matplotlib Advanced: Subplots, Annotations, Styles

You have three charts that belong together, and you keep pasting them into a slide as three separate images that never quite line up. This matplotlib subplots guide fixes that. You will build multi-panel figures, understand the Figure vs Axes split, add annotations and reference lines, share axes across panels, lay out irregular grids with GridSpec, plot two scales on one chart with dual axes, and swap the whole look with a built-in style.

“Good charts are like good code. They communicate intent clearly.”

Cole Nussbaumer Knaflic, Storytelling with Data

Last Updated: July 2026 | Tested on: Python 3.14.6, Matplotlib 3.11.0 | Difficulty: Advanced | Reading Time: 20 minutes

Real analysis almost never fits in one chart. You want a before-and-after pair. A grid of distributions, one per feature. Revenue, customers, and churn stacked so they share a timeline. Matplotlib subplots handle all of this, and the whole system gets easy once one idea clicks: the difference between a Figure and an Axes.

Think of an art studio. The Figure is the big sheet of paper pinned to the wall, the whole canvas. An Axes is one framed drawing on that sheet. You can pin a single drawing, or you can lay out a grid of them. Here is the line that trips up most beginners: the Figure is NOT the plot, the Axes IS the plot. You set the figure size and save the figure, but you draw on an Axes. Almost every method you will call in this post, such as plot, set_title, legend, and annotate, belongs to an Axes object, not to the Figure.

Here is where this pays off. Niranjan, a 27-year-old analyst, had to present quarterly results: revenue, customer count, and churn rate, all on one slide. Three separate images would not tell the story because nothing lined up. One figure with three stacked subplots sharing the same x-axis (the quarter) told the whole narrative at a glance: revenue grew, customers grew faster, but churn crept up too, a quiet warning sign. That is the pattern this post teaches.

Plot DecorationsTitleax.set_title()Legendax.legend()Gridax.grid()Spinestop, bottom,left, rightAxis ElementsX-Axisax.set_xlabel()X Major TicksX Tick LabelsY-Axisax.set_ylabel()Y Major TicksY Tick LabelsFigureplt.figure() orplt.subplots()The entire canvasAxes 1Individual plotAxes 2Another plotPython Matplotlib: Anatomy of a Figure, from Canvas to Axes to Spines

Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.

Read the diagram top down. The Figure (purple) sits at the top, the whole canvas. It holds one or more Axes (the green and blue boxes), and each Axes is a real plot. Everything else, the title, the x-axis and y-axis labels, the legend, the grid, the ticks, and the four spines (the box edges), are parts of a single Axes. Notice the method next to each box: ax.set_title(), ax.set_xlabel(), ax.legend(), ax.grid(). They all start with ax because you call them on an Axes. Keep this picture in your head and the rest of the post is just filling in the panels.

Prerequisites

Finish the Matplotlib basics tutorial first, so you are comfortable with plt.plot, bars, scatters, and histograms on a single chart. You also want NumPy from the NumPy intro, since the examples generate sample data with it. Install both inside a virtual environment with pip install matplotlib numpy. Everything below was run on Python 3.14.6 with Matplotlib 3.11.0 (latest stable at the time of writing) and NumPy 2.4.6.

Creating a Grid of Subplots

The workhorse of matplotlib subplots is plt.subplots(rows, cols). It builds the Figure and a grid of Axes in one call and hands both back to you. The grid comes back as a NumPy array, so you reach into it with row and column indexes: axes[0, 0] is top-left, axes[1, 1] is bottom-right. Think of it like seats in a small cinema: row first, then seat. Here is a 2×2 dashboard with four different chart types, one per panel.

📄 subplots.py: a 2×2 dashboard, one chart type per panel

import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(42)

# plt.subplots returns the Figure AND a 2x2 array of Axes
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# Top-left: line chart
x = np.linspace(0, 4*np.pi, 200)
axes[0, 0].plot(x, np.sin(x), label="sin", color="#50fa7b")
axes[0, 0].plot(x, np.cos(x), label="cos", color="#ff79c6")
axes[0, 0].set_title("Trig Functions")
axes[0, 0].legend()

# Top-right: histogram
data = rng.normal(0, 1, 1000)
axes[0, 1].hist(data, bins=30, color="#8be9fd", edgecolor="white")
axes[0, 1].set_title("Normal Distribution")

# Bottom-left: scatter
x_scatter = rng.uniform(0, 10, 50)
y_scatter = 2 * x_scatter + rng.normal(0, 3, 50)
axes[1, 0].scatter(x_scatter, y_scatter, alpha=0.6, color="#ffb86c")
axes[1, 0].set_title("Scatter with Trend")

# Bottom-right: bar
categories = ["A", "B", "C", "D"]
values = [25, 40, 30, 55]
axes[1, 1].bar(categories, values, color=["#50fa7b", "#8be9fd", "#ffb86c", "#ff79c6"])
axes[1, 1].set_title("Category Comparison")

fig.suptitle("Four Chart Dashboard", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.savefig("subplot_grid.png", dpi=150)
print("Saved subplot_grid.png")
print("Figure size:", fig.get_size_inches())
print("Number of axes:", len(fig.axes))

▶ Output

Saved subplot_grid.png
Figure size: [12. 10.]
Number of axes: 4

What happened here: One call to plt.subplots(2, 2) created the Figure and all four Axes at once. The Figure is the shared canvas; fig.suptitle writes one title across the top, and fig.get_size_inches() confirms the 12 by 10 inch canvas. Each chart lives on its own Axes, so axes[0, 0].plot(...) draws only in the top-left and never touches the others. The line len(fig.axes) returns 4 because the Figure now owns four Axes. The last detail that saves you grief is plt.tight_layout(): without it the titles and labels overlap; with it Matplotlib spaces the panels out cleanly. We used a seeded random generator (default_rng(42)) so your histogram and scatter match the saved image instead of changing on every run.

Sharing Axes Across Panels

This is Niranjan’s quarterly dashboard from the intro. Picture three notepads stacked on a desk, all lined up against one ruler laid along the bottom edge: you read the scale once and every page measures against it. Sharing an axis does the same thing. When several matplotlib subplots plot against the same x-axis (here, the quarter), repeating the x tick labels on every panel is clutter. Pass sharex=True and Matplotlib locks the panels to the same x-range and prints the labels only once, on the bottom panel. It is the difference between three loose charts and one tidy stacked story.

📄 shared_axes.py: three stacked panels sharing one x-axis

import matplotlib.pyplot as plt

quarters = ["Q1", "Q2", "Q3", "Q4"]
revenue = [120, 135, 148, 162]
customers = [3400, 3900, 4700, 5200]
churn = [2.1, 2.4, 2.8, 3.3]

# 3 stacked panels that SHARE the same x-axis
fig, axes = plt.subplots(3, 1, figsize=(10, 9), sharex=True)

axes[0].plot(quarters, revenue, marker="o", color="#50fa7b")
axes[0].set_ylabel("Revenue ($K)")
axes[0].set_title("Niranjan's Quarterly Dashboard")

axes[1].plot(quarters, customers, marker="s", color="#8be9fd")
axes[1].set_ylabel("Customers")

axes[2].plot(quarters, churn, marker="^", color="#ff5555")
axes[2].set_ylabel("Churn (%)")
axes[2].set_xlabel("Quarter")

plt.tight_layout()
plt.savefig("shared_axes.png", dpi=150)
print("Saved shared_axes.png")
print("Panels:", len(fig.axes))
print("All three share one x-axis:", axes[0].get_shared_x_axes().joined(axes[0], axes[2]))

▶ Output

Saved shared_axes.png
Panels: 3
All three share one x-axis: True

What happened here: With plt.subplots(3, 1) the grid is a single column, so axes is a flat array and you index it with one number: axes[0], axes[1], axes[2]. Because we passed sharex=True, the three panels are linked: zoom or pan one and the others follow, and the “Q1, Q2, Q3, Q4” labels appear only on the bottom panel instead of three times. The final print line proves the link is real. Now the story reads top to bottom on one shared timeline: revenue up, customers up faster, churn creeping up too. That is exactly the warning sign Niranjan needed on his slide.

Annotations and Reference Lines

A chart that just shows the data is fine. A chart that points at the one thing you want the reader to notice is better. Annotations are little notes with arrows, like sticking a Post-it on the exact spot of a printed graph. Reference lines and shaded bands give the reader a baseline to compare against. Here we mark the peak month, draw the average as a dashed line, and shade the growth phase.

📄 annotations.py: highlight the peak, the average, and a growth band

import matplotlib.pyplot as plt
import numpy as np

months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
          "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
revenue = [42, 45, 48, 52, 55, 67, 72, 68, 75, 81, 77, 89]

fig, ax = plt.subplots(figsize=(12, 6))
ax.plot(months, revenue, marker="o", linewidth=2, color="#bd93f9")

# Annotate the peak with an arrow
max_idx = np.argmax(revenue)
ax.annotate(f"Peak: ${revenue[max_idx]}K",
            xy=(max_idx, revenue[max_idx]),
            xytext=(max_idx - 2, revenue[max_idx] + 5),
            arrowprops=dict(arrowstyle="->", color="#50fa7b"),
            fontsize=12, color="#50fa7b", fontweight="bold")

# Horizontal reference line at the average
ax.axhline(y=np.mean(revenue), color="#ff5555", linestyle="--",
           alpha=0.7, label=f"Avg: ${np.mean(revenue):.0f}K")

# Shade the growth phase (Jun to Sep)
ax.axvspan(5, 8, alpha=0.1, color="#50fa7b", label="Growth phase")

ax.set_title("Annual Revenue with Annotations", fontsize=14)
ax.set_ylabel("Revenue ($K)")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("annotated_chart.png", dpi=150)
print("Saved annotated_chart.png")
print("Peak month:", months[max_idx], "value:", revenue[max_idx])
print("Average revenue:", round(float(np.mean(revenue)), 2))

▶ Output

Saved annotated_chart.png
Peak month: Dec value: 89
Average revenue: 64.25

What happened here: ax.annotate takes two points. xy is where the arrow tip lands (the data point you are calling out) and xytext is where the text sits, offset a bit so it does not cover the line. np.argmax(revenue) found the index of the biggest value, which the print confirms is December at 89. ax.axhline draws a horizontal line at the average (64.25, also confirmed by the print), and ax.axvspan paints a faint band between the 6th and 9th months to mark the growth phase.

Both the line and the band carry a label, so ax.legend() picks them up automatically. The numbers in the labels are computed, not typed by hand, so they can never drift out of sync with the data.

Irregular Layouts with GridSpec

A clean 2×2 grid of matplotlib subplots is great until you want one panel to be bigger than the rest. Picture a newspaper front page: one wide headline story across the top, two smaller columns underneath. GridSpec gives you that. You define a grid, then let a single Axes span several cells with slice syntax. gs[0, :] means “row 0, all columns”, so that Axes stretches the full width.

📄 gridspec.py: one wide panel on top, two normal panels below

import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(7)

fig = plt.figure(figsize=(12, 8))
gs = fig.add_gridspec(2, 2)

# Top row: one wide plot spanning BOTH columns
ax_top = fig.add_subplot(gs[0, :])
days = np.arange(1, 31)
ax_top.plot(days, np.cumsum(rng.normal(2, 1, 30)), color="#bd93f9")
ax_top.set_title("Monthly trend (spans full width)")

# Bottom-left and bottom-right: two normal cells
ax_left = fig.add_subplot(gs[1, 0])
ax_left.bar(["A", "B", "C"], [12, 19, 7], color="#50fa7b")
ax_left.set_title("Category totals")

ax_right = fig.add_subplot(gs[1, 1])
ax_right.hist(rng.normal(0, 1, 500), bins=20, color="#ffb86c", edgecolor="white")
ax_right.set_title("Distribution")

plt.tight_layout()
plt.savefig("gridspec_layout.png", dpi=150)
print("Saved gridspec_layout.png")
print("Total axes in figure:", len(fig.axes))

▶ Output

Saved gridspec_layout.png
Total axes in figure: 3

What happened here: We started from a bare plt.figure() and called fig.add_gridspec(2, 2) to define a 2 by 2 grid of cells, but we never had to fill all four. The top Axes claimed both top cells with gs[0, :] (the colon means “every column”), so it spans the full width like a headline. The two bottom Axes took one cell each with gs[1, 0] and gs[1, 1]. The Figure ends up with three Axes, not four, which the print confirms. This is the layout you see in scientific papers and dashboards all the time: one hero chart up top, supporting detail beneath.

Two Scales on One Chart: Dual Axes

Sometimes two series belong on the same chart but live on wildly different scales. Revenue in thousands of dollars and conversion rate in single-digit percents, for example. Plot them on one y-axis and the small one flatlines against the big one. The fix is a second y-axis on the right, created with ax.twinx(). Same x-axis, two independent y-axes. It is like a clock face that shows hours on the outer ring and minutes on the inner one: one dial, two scales.

📄 dual_axis.py: revenue and conversion rate on two y-axes

import matplotlib.pyplot as plt

months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
revenue = [42, 45, 48, 52, 55, 67]            # thousands of dollars
conversion = [1.8, 2.1, 2.0, 2.6, 2.9, 3.4]   # percent, tiny numbers

fig, ax1 = plt.subplots(figsize=(10, 6))

# Left axis: revenue (big numbers)
ax1.plot(months, revenue, marker="o", color="#50fa7b")
ax1.set_xlabel("Month")
ax1.set_ylabel("Revenue ($K)", color="#50fa7b")
ax1.tick_params(axis="y", labelcolor="#50fa7b")

# Right axis: conversion rate (small numbers), shares the x-axis
ax2 = ax1.twinx()
ax2.plot(months, conversion, marker="s", color="#ff79c6")
ax2.set_ylabel("Conversion (%)", color="#ff79c6")
ax2.tick_params(axis="y", labelcolor="#ff79c6")

ax1.set_title("Revenue vs Conversion (two different scales)")
plt.tight_layout()
plt.savefig("dual_axis.png", dpi=150)
print("Saved dual_axis.png")
print("ax1 (left) ylabel:", ax1.get_ylabel())
print("ax2 (right) ylabel:", ax2.get_ylabel())
print("Both axes share an x-axis:", ax1.get_shared_x_axes().joined(ax1, ax2))

▶ Output

Saved dual_axis.png
ax1 (left) ylabel: Revenue ($K)
ax2 (right) ylabel: Conversion (%)
Both axes share an x-axis: True

What happened here: ax1.twinx() made a brand new Axes (ax2) that sits on top of ax1 and reuses its x-axis but gets its own y-axis on the right. The print line confirms the two share an x-axis. We colored each y-label and its ticks to match its line, so the reader instantly knows green numbers read off the left and pink numbers read off the right. One honest warning: dual-axis charts can mislead, because you control where each scale starts and you can make two unrelated lines look like they move together. Use them when both series genuinely share the x-axis and the reader needs to compare their shapes, not their absolute heights.

Built-in Styles

You do not have to hand-tune every color and font. Matplotlib ships with ready-made themes. One line, plt.style.use("ggplot"), restyles every chart after it. It is like applying a filter to a photo: the data stays the same, the whole look changes. Run this to see exactly which styles your install offers, because the list grows between versions.

📄 styles.py: list every built-in style available to you

import matplotlib.pyplot as plt

# List the styles your Matplotlib install ships with
print("Available styles:")
for s in sorted(plt.style.available):
    print(f"  {s}")

▶ Output (Matplotlib 3.11.0)

Available styles:
  Solarize_Light2
  bmh
  classic
  dark_background
  fast
  fivethirtyeight
  ggplot
  grayscale
  petroff10
  petroff6
  petroff8
  seaborn-v0_8
  seaborn-v0_8-bright
  seaborn-v0_8-colorblind
  seaborn-v0_8-dark
  seaborn-v0_8-dark-palette
  seaborn-v0_8-darkgrid
  seaborn-v0_8-deep
  seaborn-v0_8-muted
  seaborn-v0_8-notebook
  seaborn-v0_8-paper
  seaborn-v0_8-pastel
  seaborn-v0_8-poster
  seaborn-v0_8-talk
  seaborn-v0_8-ticks
  seaborn-v0_8-white
  seaborn-v0_8-whitegrid
  tableau-colorblind10

What happened here: plt.style.available is just a list of strings, the names you can pass to plt.style.use(...). A few worth knowing: seaborn-v0_8-darkgrid is a clean, minimal default for data work; ggplot mimics R’s famous look; fivethirtyeight is bold and editorial; dark_background suits dark dashboards. The petroff6, petroff8, and petroff10 styles are newer, color-blind-friendly palettes (the number is how many distinct colors the cycle holds). Note the seaborn-v0_8- prefix: older guides call these just seaborn-darkgrid, but Matplotlib renamed them years ago to pin the look to a specific Seaborn version, so always use the v0_8 spelling.

Two tips: apply a style before you create any Axes, and wrap it in with plt.style.context("ggplot"): if you only want it for one chart instead of the rest of the program.

When You Will Use These

These matplotlib subplots patterns are not academic. Here is where they show up in real day-to-day work:

  • Feature exploration before modeling: when you have a dataset with a dozen columns, a 3×4 grid of histograms (one per column) tells you in one glance which features are skewed, which have outliers, and which look usable. This is the first thing you do in any Exploratory Data Analysis (EDA) workflow.
  • Stakeholder dashboards: Niranjan’s stacked, shared-axis panels are exactly how you put revenue, customers, and churn on one slide so a manager sees the whole quarter in three seconds, not three clicks.
  • Before-and-after comparisons: two side-by-side panels with sharey=True let you show “raw data” versus “after cleaning” on the same y-scale, so the improvement is honest and obvious.
  • Reports that need annotations: the moment a chart goes into a PDF or a slide, you want the peak called out, the average drawn in, and the important window shaded. Readers will not study your data; the annotations do the studying for them.

Common Mistakes

Mistake 1: Cramming everything onto one Axes

Ten lines on a single chart is a tangle of spaghetti nobody can read. The fix is “small multiples”: split the lines into a grid of tiny panels, one line each, sharing the same axes so the shapes are comparable. Here is the wrong way and the right way, both runnable.

🚫 Wrong: ten lines on one Axes

import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(1)
x = np.arange(50)
series = {f"line_{i}": np.cumsum(rng.normal(0, 1, 50)) for i in range(10)}

fig, ax = plt.subplots(figsize=(10, 6))
for name, y in series.items():
    ax.plot(x, y, label=name)           # 10 overlapping lines, unreadable
ax.legend(ncol=2)
ax.set_title("Ten lines on one chart (unreadable)")
plt.tight_layout()
plt.savefig("overcrowded.png", dpi=150)
print("Saved overcrowded.png with", len(ax.lines), "lines on a single Axes")

✅ Correct: small multiples, one line per panel

import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(1)
x = np.arange(50)
series = {f"line_{i}": np.cumsum(rng.normal(0, 1, 50)) for i in range(10)}

# 2x5 grid, one line per panel, shared scales for fair comparison
fig, axes = plt.subplots(2, 5, figsize=(15, 6), sharex=True, sharey=True)
for ax, (name, y) in zip(axes.flat, series.items()):
    ax.plot(x, y, color="#bd93f9")
    ax.set_title(name, fontsize=9)

fig.suptitle("Ten panels, one line each (readable)", fontsize=14)
plt.tight_layout()
plt.savefig("small_multiples.png", dpi=150)
print("Grid shape:", axes.shape, "=> total panels:", axes.size)
print("Lines per panel:", len(axes[0, 0].lines))

▶ Output (correct version)

Grid shape: (2, 5) => total panels: 10
Lines per panel: 1

Why: axes.flat flattens the 2×5 grid into a simple iterator, so a single for loop can walk every panel in order. sharex=True, sharey=True locks all ten panels to the same scales, which is what makes the comparison fair (a taller line really is bigger, not just zoomed in). Rule of thumb: if a single chart has more than five or six things competing for attention, split it into panels.

Mistake 2: Forgetting tight_layout, so labels collide

🚫 Wrong

fig, axes = plt.subplots(2, 2)
# ... draw on each panel ...
plt.savefig("dashboard.png")   # titles and tick labels overlap and clip

✅ Correct

fig, axes = plt.subplots(2, 2)
# ... draw on each panel ...
plt.tight_layout()             # spaces panels so nothing collides
plt.savefig("dashboard.png")

Why: Matplotlib places panels first, then you add titles and labels, so by default they can overlap or get clipped at the figure edge. Call plt.tight_layout() right before saving and it recomputes the spacing. For complex figures, fig.set_layout_engine("constrained") (or constrained_layout=True in plt.subplots) is the stronger, modern alternative that also handles shared colorbars cleanly.

Practice Exercises

  1. Exercise 1: Build a 2×2 grid for one week of step counts for four friends (say Aditi, Anvay, Aviraj, and Niranjan): a line of daily steps, a bar of weekly totals, a histogram of all step values, and a scatter of steps versus calories. Give each panel a title and call tight_layout().
  2. Exercise 2: Take any single line chart and annotate it: mark the maximum point with an arrow, draw the average as a dashed horizontal line, and shade one interesting window with axvspan. Put the average value in the legend label using an f-string so it stays accurate.
  3. Exercise 3: Use GridSpec to build a “headline” layout: one wide trend chart spanning the full top row, and two supporting charts below it. Then re-run it after plt.style.use("fivethirtyeight") and compare the look.

Conclusion

You started with three charts that never lined up and finished with a full matplotlib subplots toolkit for multi-panel figures. The one idea that carries everything is the Figure versus Axes split: the Figure is the canvas, the Axes is the plot you actually draw on. From there you built a 2×2 dashboard with plt.subplots, linked panels with sharex so a story reads top to bottom, called out the important points with annotations and reference lines, broke out of the rigid grid with GridSpec, layered two scales with twinx, and reskinned the whole thing with a built-in style. That is the exact set of moves behind almost every dashboard and report you will ever ship.

Next up is Seaborn, which sits on top of Matplotlib and turns a lot of this hand-work into one-liners for statistical charts like heatmaps and distributions. Everything you learned here still applies underneath, because Seaborn draws onto the same Figure and Axes objects. For the full path from beginner to job-ready, browse the Python + AI/ML tutorial series home.

Frequently Asked Questions

What is the difference between Figure and Axes in Matplotlib?

The Figure is the whole canvas, like the sheet of paper. An Axes is one plot drawn on that canvas, with its own title, x and y axes, and legend. One Figure can hold many Axes (subplots). You set figure-level things like size and saving on the Figure, but you call plot, set_title, legend, and annotate on an Axes.

How do I create matplotlib subplots?

Call plt.subplots(rows, cols). It returns the Figure and a NumPy array of Axes in one go: fig, axes = plt.subplots(2, 2). Then index the array to draw on each panel, for example axes[0, 0].plot(…) for the top-left. For a single column or row the array is flat, so you index with one number like axes[0].

How do I share axes between subplots?

Pass sharex=True or sharey=True to plt.subplots, for example fig, axes = plt.subplots(2, 1, sharex=True). Matplotlib then locks the panels to the same range and prints the shared tick labels only once, which removes clutter and makes the panels directly comparable.

How do I create unequal subplot layouts with GridSpec?

Use add_gridspec, then let an Axes span several cells with slice syntax: gs = fig.add_gridspec(2, 2); ax1 = fig.add_subplot(gs[0, :]) makes one wide panel across the top row, and ax2 = fig.add_subplot(gs[1, 0]) is a normal cell below. The colon means all columns, so gs[0, :] spans the full width.

How do I add a second y-axis to a Matplotlib chart?

Call ax2 = ax1.twinx(). The new Axes shares the x-axis with ax1 but gets an independent y-axis on the right, which is handy when two series have very different scales like revenue in thousands and conversion rate in percent. Color each y-label to match its line so readers know which scale to read.

Why do my subplot titles and labels overlap?

Matplotlib places the panels before you add titles and labels, so by default they can collide. Call plt.tight_layout() right before saving to recompute the spacing. For complex figures, constrained_layout=True in plt.subplots (or fig.set_layout_engine(‘constrained’)) is the stronger modern option.

Interview Questions on Matplotlib Subplots

Interviewers rarely ask for definitions. They ask what happens in situations like these.

Q: When does plt.subplots return a single Axes, a 1D array, or a 2D array, and why does that matter?

With plt.subplots(1, 1) you get one bare Axes object, not an array. With a single row or single column, like plt.subplots(3, 1), you get a flat 1D array you index with one number. With multiple rows and columns you get a 2D array indexed as axes[row, col]. It matters because code written for the 2D case breaks on a 1×1 figure. If you want a consistent shape every time, pass squeeze=False and you always get a 2D array, even for one panel.

Q: What is the difference between plt.tight_layout() and constrained_layout, and which should you reach for?

Both fix overlapping titles, labels, and ticks by recomputing spacing. tight_layout() runs once, when you call it, so if you add elements afterward it can go stale. constrained_layout (set via constrained_layout=True in plt.subplots or fig.set_layout_engine("constrained")) is a live layout engine that adjusts continuously and handles shared colorbars and suptitles more cleanly. For simple figures either is fine; for dense grids or shared colorbars, prefer constrained layout.

Q: How do you combine legends from a dual-axis (twinx) chart into a single legend box?

Because ax1 and ax2 are separate Axes, calling legend() on each gives you two boxes. Collect the handles and labels from both and pass them to one legend: h1, l1 = ax1.get_legend_handles_labels(), h2, l2 = ax2.get_legend_handles_labels(), then ax1.legend(h1 + h2, l1 + l2). That merges both series into a single legend so the reader is not hunting across two boxes.

Q: You render a 4×4 grid of subplots and the y-axis labels are clipped at the figure edge even after tight_layout(). What do you check first?

First confirm the figure is physically large enough: 16 panels in a default-size figure leaves almost no room, so bump figsize. Next switch from tight_layout() to constrained_layout=True, which handles crowded grids better. If long tick labels are the culprit, rotate them with tick_params(rotation=45) or shorten them. A stray suptitle with no reserved space can also push content off the edge, so give it room or let constrained layout manage it.

Q: A colleague shows two unrelated metrics on a twinx dual-axis chart and claims they “move together.” Why is that risky, and how do you sanity-check it?

Dual axes let you set each y-scale independently, so you can slide two lines until they appear to track each other even when they do not. The visual correlation is an artifact of the chosen scales, not the data. To sanity-check, plot both series on a shared scale or normalize them (for example to a 0 to 1 range) and see if the shapes still line up, or just compute the actual correlation. Reserve dual axes for cases where the reader genuinely needs to compare shapes over a shared x-axis, not absolute heights.

Q: When would you use GridSpec instead of a plain plt.subplots grid?

Use plt.subplots when every panel is the same size in a regular grid, which covers most dashboards. Reach for GridSpec when panels need unequal sizes or spans: one wide “headline” chart across the full top row with smaller supporting charts below, or a tall panel beside two short ones. You define the grid with fig.add_gridspec(rows, cols) and let an Axes claim several cells with slice syntax like gs[0, :] for a full-width row.

Go deeper: Matplotlib documentation covers every edge case of this topic.

Previous: Matplotlib Basics: Line, Bar, Scatter, Histogram

Next: Seaborn: Statistical Visualization, Heatmaps, Distributions

Series Home: Python + AI/ML Tutorial Series

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *