matplotlib introduction content is everywhere, but two things confuse nearly every beginner and rarely get resolved clearly: the Figure/Axes relationship, and whether to use pyplot's implicit style or the explicit object-oriented API. This matplotlib pyplot tutorial resolves both directly, upfront, and uses the recommended object-oriented approach throughout the rest of the article.
What Matplotlib Is and the Two Ways to Use It
What Matplotlib actually does
Matplotlib graphs data on Figures — windows, Jupyter widgets, or other output targets — each of which can contain one or more Axes: an area where points are specified in x-y coordinates. Understanding this Figure-contains-Axes relationship precisely, covered in full in Section 2, is genuinely the foundation for everything else in Matplotlib.
The key upfront decision
Every beginner faces this choice, usually without realizing it's a choice at all: matplotlib.pyplot is a collection of functions that make Matplotlib work somewhat like MATLAB, offering an implicit style — you call plt.plot(...), and Matplotlib figures out which Figure and Axes you mean automatically, behind the scenes. This is generally less verbose, but also less flexible, than explicitly creating Figures and Axes yourself and calling methods directly on them — the object-oriented style.
What this article does
This article uses the recommended object-oriented approach throughout — explicitly creating Figures and Axes, and calling methods directly on the Axes object — while still explaining where the simpler pyplot shortcuts genuinely fit, for quick, one-off, throwaway plots where the extra explicitness wouldn't pay for itself.
The Anatomy of a Matplotlib Plot: Figure, Axes, and Axis
This terminology is worth nailing down precisely before writing any code, since these exact terms are used consistently throughout Matplotlib's own documentation, and getting them straight from the start prevents a lot of confusion later.
Figure: the overarching container
The Figure is the entire canvas for a visualization — the overarching container holding every plot element: one or more Axes, titles, legends, everything. Think of it as the whole window or image file, in its entirety.
Axes: the actual plotting area
The Axes — note the capitalization and the fact that it's spelled the same as the plural of "axis," which is genuinely a common source of confusion — is the actual area within the figure where data gets plotted. A single Figure can contain multiple Axes (multiple subplots, arranged in a grid), each one an independent plotting area with its own data, its own title, and its own axis labels.
Worth repeating explicitly, since it trips up nearly everyone at first: "Axes" in Matplotlib refers to this specific plotting-area concept — a single object representing one chart within a figure — not simply the plural of "axis" (the x-axis and y-axis). A Figure with one subplot has one Axes object (singular, despite the "-es" ending); a Figure with a 2×3 grid of subplots has six Axes objects.
Axis: the x-axis and y-axis specifically
The Axis (singular, genuinely referring to just one of the x-axis or y-axis) defines the limits, tick locations, and labels for that specific dimension of a given Axes. Every Axis has an associated tick locator and formatter, which choose where along that axis to actually place tick marks and how to label them — usually handled automatically and sensibly by Matplotlib, but adjustable directly when you need finer control.
Titles, labels, and legends
Beyond the Figure/Axes/Axis structure itself, a plot is decorated with titles (naming the whole Axes or Figure), labels (naming the x-axis and y-axis specifically), and legends (identifying which plotted line or marker corresponds to which piece of data) — all covered concretely with working code in Section 4.
Creating a Figure and Axes: The Recommended Pattern
The single most useful shorthand
import matplotlib.pyplot as plt
fig, ax = plt.subplots()plt.subplots() creates both a Figure and an Axes together, in one line, returning them as a tuple — this is the generally recommended way to start nearly any new plot, even a simple one with just a single Axes.
Calling methods directly on the Axes object
import matplotlib.pyplot as plt
x = [1, 2, 3, 4, 5]
y = [2, 4, 6, 8, 10]
fig, ax = plt.subplots()
ax.plot(x, y, marker="o")
ax.set_title("A Simple Line Chart")
ax.set_xlabel("X values")
ax.set_ylabel("Y values")
plt.show()Notice every plotting and labeling call — ax.plot(), ax.set_title(), ax.set_xlabel() — is called directly on the ax object, rather than the implicit plt.plot(), plt.title() versions you'll see in many older tutorials. Building this habit from the start is genuinely worth the small extra bit of typing: it's explicit about exactly which Axes you're modifying, which becomes essential the moment you're working with more than one Axes at once, covered next.
Creating multiple subplots in a grid
fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(12, 6))
axes[0, 0].plot(x, y)
axes[0, 0].set_title("Plot 1")
axes[1, 2].plot(x, [v ** 2 for v in y])
axes[1, 2].set_title("Plot 2")
plt.show()plt.subplots(nrows=2, ncols=3) creates a single Figure containing a 2×3 grid of six separate Axes objects, returned as a NumPy array (exactly the array indexing covered in the earlier NumPy articles in this series) — axes[0, 0] is the top-left subplot, axes[1, 2] is the bottom-right, and each can be plotted into and labeled entirely independently of the others. This is the natural next step once single-plot basics feel comfortable, and it's precisely where the object-oriented style covered throughout this article genuinely earns its keep — the implicit plt. style becomes considerably more confusing and error-prone the moment more than one Axes is involved, since it's never fully clear which Axes an implicit plt.plot() call is actually targeting.
Basic Plot Types: Line Charts and Beyond
A simple line chart
import matplotlib.pyplot as plt
x = [0, 1, 2, 3, 4, 5]
y = [v ** 2 for v in x] # a simple quadratic relationship
fig, ax = plt.subplots()
ax.plot(x, y)
plt.show()ax.plot(x, y) is the simplest, most common starting point in Matplotlib — connecting data points with a line to show a trend, here a basic quadratic relationship.
Making the plot actually readable
fig, ax = plt.subplots()
ax.plot(x, y, label="y = x²")
ax.plot(x, [v * 3 for v in x], label="y = 3x")
ax.set_title("Comparing Two Relationships")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.legend()
plt.show()ax.set_title(), ax.set_xlabel(), and ax.set_ylabel() add the essential context that turns a bare, unlabeled plot into something someone else can actually interpret without asking you what it means. ax.legend() — combined with a label= argument on each individual ax.plot() call — automatically generates a legend identifying which line corresponds to which labeled data series, genuinely essential the moment a plot shows more than one thing at once.
Beyond line charts
Matplotlib supports many plot types beyond line charts — scatter plots (ax.scatter()), bar charts (ax.bar()), histograms (ax.hist()), and considerably more — all built on the exact same Figure/Axes foundation covered throughout this article. Once the Figure/Axes relationship and the object-oriented calling pattern genuinely feel comfortable, switching between plot types is largely a matter of calling a different method on the same ax object, rather than learning an entirely new mental model each time — a topic covered in dedicated depth in a later article in this series.
Saving Figures and Practical Habits Going Forward
Saving a finished plot to a file
fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_title("My Chart")
fig.savefig("my_chart.png")fig.savefig("filename.png") saves the entire Figure — including every Axes it contains — directly to a file, in whatever format the file extension implies (.png, .pdf, .svg, and several others are all supported).
Controlling output resolution
fig.savefig("my_chart.png", dpi=300)The dpi (dots per inch) parameter controls the output resolution — genuinely important once a figure needs to go into a printed report or a presentation, where a low default resolution can look noticeably blurry or pixelated once scaled up. dpi=300 is a common, safe choice for print-quality output; the default (typically 100) is generally fine for on-screen viewing alone.
Sizing a figure appropriately from the start
fig, ax = plt.subplots(figsize=(10, 6)) # width, height, in inchesfigsize, passed directly to plt.subplots(), sets the Figure's dimensions in inches — genuinely worth setting deliberately from the very start of creating a plot, rather than generating a plot at Matplotlib's default size and attempting to resize it afterward, which tends to be considerably more awkward and can throw off font sizes, marker sizes, and spacing relative to the rest of the figure.
Closing practical guidance
Default to the object-oriented fig, ax = plt.subplots() pattern for anything beyond a single, immediately-discarded, throwaway plot. It scales cleanly to multi-panel figures (as shown in Section 3), it's explicit about exactly which Axes any given call is targeting, and it avoids the implicit "current figure" state that the bare plt. functions rely on internally — a source of genuine confusion for beginners who call a plt. function expecting it to affect one specific plot, only to find it silently affected a different one, because Matplotlib's notion of the "current" figure or Axes wasn't what they assumed it was. The small amount of extra typing the object-oriented style requires pays for itself almost immediately, the moment a project grows beyond its very first single plot.