Introduction to Matplotlib and the pyplot Interface

1

Introduction to Matplotlib and the pyplot Interface

▶

Matplotlib is the foundational plotting library of the Python scientific ecosystem. Created by John D. Hunter in 2003, it was originally designed to replicate the plotting capabilities of MATLAB in a free, open-source environment. Over the two decades since its release, it has grown into an extraordinarily feature-rich library that powers visualizations in academic research, data science, engineering, and finance. Understanding Matplotlib deeply means understanding not just how to draw a line on a chart, but how the library thinks about the structure of a visualization — from the outermost window all the way down to individual tick marks on an axis.

At its heart, Matplotlib works by constructing a hierarchy of objects. Every plot you make is composed of a Figure, which is the top-level container (think of it as the canvas or the page), one or more Axes objects inside the Figure (each Axes is an individual plot panel with its own coordinate system), and then the actual visual elements — lines, markers, text, patches — that live inside each Axes. This object hierarchy is fundamental to everything Matplotlib does, and all the convenience functions you call eventually manipulate objects in this hierarchy.

What is Matplotlib?

Matplotlib is a comprehensive library for creating static, animated, and interactive visualizations in Python. It is distributed as an open-source package and is installable via pip or conda. At the most basic level it lets you draw a line chart or a scatter plot with just a couple of lines of code, but its true power lies in the granular control it gives you over every visual element on the screen.

One of Matplotlib's most important practical qualities is its seamless integration with NumPy arrays and pandas DataFrames. Because nearly all numerical Python work involves NumPy arrays — whether directly or via libraries like pandas that are built on top of NumPy — the fact that Matplotlib accepts these structures natively is enormously convenient. You do not need to convert data to lists or any special format. Consider this example:

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 2 * np.pi, 300)   # NumPy array of 300 evenly spaced values
y = np.sin(x)                          # NumPy array of sine values

plt.plot(x, y)
plt.show()

Here x and y are plain NumPy arrays and plt.plot() accepts them directly. The same works with a pandas Series or a column of a DataFrame, making it trivial to plot data you have already loaded and cleaned in pandas.

Matplotlib also supports a wide range of output formats, which makes it suitable for many different workflows. When you call plt.savefig(), the file format is inferred from the extension you provide. Common raster formats include PNG (ideal for web and screen display) and JPEG (smaller file size, lossy compression). Vector formats include PDF (excellent for print and LaTeX documents) and SVG (scalable vector graphics, easily embedded in web pages or edited in tools like Inkscape). Because vector formats store mathematical descriptions of shapes rather than pixel grids, figures saved as PDF or SVG remain crisp at any zoom level — critically important for publication-quality figures.

Perhaps most importantly for advanced users, Matplotlib is highly customizable. Every visual element — the font family and size on axis labels, the width and color and dash pattern of a plotted line, the position and number of tick marks, the background color of the plot area, the spacing between subplots — can be controlled programmatically. This fine-grained control distinguishes Matplotlib from higher-level libraries like Seaborn or Plotly, which make it easy to produce attractive defaults quickly but can be harder to customize precisely. Matplotlib, by contrast, gives you the full toolkit to produce exactly the figure you envision.

The pyplot Module

While Matplotlib's full object-oriented API is powerful, the most common way beginners (and many experienced practitioners) interact with the library is through pyplot, a module within Matplotlib that provides a MATLAB-style procedural interface. It is conventionally imported as follows:

import matplotlib.pyplot as plt

The alias plt is nearly universal in the Python community and you will see it in virtually every tutorial, textbook, and Stack Overflow answer. Using it saves typing and makes code immediately recognizable to other Python programmers.

The key concept that makes pyplot work is that it maintains an internal notion of the current figure and the current axes. When you call a pyplot function, it automatically operates on whichever figure and axes are currently active, so you do not need to explicitly pass references to these objects on every call. This significantly reduces boilerplate code in simple, single-figure scripts. For example:

import matplotlib.pyplot as plt

plt.plot([1, 2, 3, 4], [10, 20, 25, 30])   # plots on the current axes
plt.xlabel('Time (s)')                       # labels the x-axis of the current axes
plt.title('Sample Plot')                     # sets the title of the current axes
plt.show()                                   # displays the current figure

In this script, pyplot created a figure and axes automatically on the first call to plt.plot(), and all subsequent calls operated on that same axes without you ever needing to reference it directly. This is convenient for quick, exploratory work.

The three most commonly used pyplot functions are worth understanding individually. plt.plot() is the general-purpose line-and-marker plotting function. It accepts x and y data (as lists, NumPy arrays, or pandas Series), along with keyword arguments for styling (color, linewidth, linestyle, marker shape, etc.). plt.show() renders the current figure and displays it — in an interactive environment like a Jupyter Notebook this may happen automatically, but in a script it is necessary to call plt.show() explicitly to see the output. plt.savefig() writes the current figure to disk, and must be called before plt.show() in a script because calling plt.show() first clears the figure.

For more complex visualization needs — multiple subplots with different data, figures embedded in GUI applications, or careful reuse of axes objects across functions — pyplot can be combined with the object-oriented (OO) Matplotlib API. In this approach, you explicitly create and store references to Figure and Axes objects and call methods on those objects rather than going through the pyplot state machine. The two styles are not mutually exclusive; a common hybrid pattern is to use pyplot to create the figure and axes objects and then work with those objects directly:

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()          # create Figure and Axes, get references to both

x = np.linspace(0, 10, 100)
ax.plot(x, np.sin(x), label='sin(x)')
ax.plot(x, np.cos(x), label='cos(x)')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title('Trig Functions')
ax.legend()

plt.show()

Here fig is a Figure object and ax is an Axes object. Methods like ax.plot(), ax.set_xlabel(), and ax.legend() are called directly on the Axes, which is the recommended approach whenever your code grows beyond a single simple plot.

Figures and Axes Objects

Understanding the difference between a Figure and an Axes is arguably the most important conceptual step in learning Matplotlib, because confusion between these two objects is the source of many common errors.

A Figure is the entire window or page. It is the outermost container. You create one explicitly with plt.figure(), which instantiates a new Figure object and immediately makes it the current figure in pyplot's state machine:

fig = plt.figure(figsize=(10, 6))   # 10 inches wide, 6 inches tall

The figsize argument controls the physical dimensions of the figure in inches, which matters when saving to a file because the output dimensions (combined with the dpi setting) determine the pixel resolution. A figure with no axes added to it is just a blank canvas — nothing will be visible.

An Axes object is an individual plot region within a Figure. It contains its own x-axis, y-axis, title, tick marks, tick labels, data space, and all the plotted artists (lines, markers, text, images, etc.). A single Figure can contain many Axes objects arranged in any layout, which is how multi-panel figures and subplot grids are constructed. Each Axes is completely independent — it has its own axis limits, scale, labels, and coordinate system.

You can add an Axes to a Figure using fig.add_subplot() or the more convenient plt.subplots() function. When you use plt.subplots(nrows, ncols), you get back both the Figure and a NumPy array of Axes objects:

import matplotlib.pyplot as plt
import numpy as np

fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(10, 8))

# axes is a 2x2 NumPy array of Axes objects
axes[0, 0].plot([1, 2, 3], [4, 5, 6])
axes[0, 0].set_title('Top Left')

axes[0, 1].scatter([1, 2, 3], [9, 3, 7])
axes[0, 1].set_title('Top Right')

axes[1, 0].bar(['A', 'B', 'C'], [5, 8, 3])
axes[1, 0].set_title('Bottom Left')

axes[1, 1].hist(np.random.randn(1000), bins=30)
axes[1, 1].set_title('Bottom Right')

plt.tight_layout()   # automatically adjusts spacing to prevent overlapping labels
plt.show()

By accessing each Axes object through its index in the axes array, you have precise, unambiguous control over each plot panel. This is why working with Axes objects directly — rather than relying solely on pyplot's current-axes concept — becomes essential as your visualizations grow in complexity. Each Axes independently manages its own x-axis and y-axis objects (accessible as ax.xaxis and ax.yaxis), which themselves contain tick locators, tick formatters, and axis labels. This nesting is what gives Matplotlib its remarkable depth of customization.

Displaying and Saving Figures

Once you have built a figure, you need to either display it or save it to disk (or both). Understanding how these operations work correctly avoids several common frustrations.

Saving a figure is done with plt.savefig(), or equivalently with the fig.savefig() method on a Figure object (the latter is preferred when you have an explicit reference to the figure). A critical and commonly overlooked parameter is dpi, which stands for dots per inch:

plt.savefig('output_figure.png', dpi=300)

The dpi setting controls the resolution of the output raster image. The default is typically 100 dpi, which produces figures adequate for screen display but often too low-resolution for print publications, which typically require at least 300 dpi. A figure that is 8 inches wide saved at 100 dpi will be 800 pixels wide; the same figure at 300 dpi will be 2400 pixels wide — much sharper when printed. For vector formats (PDF, SVG), the dpi setting is less relevant because the output is resolution-independent, though it still affects any raster elements embedded within the vector file (such as images plotted with imshow()). Additional useful arguments to savefig() include bbox_inches='tight', which trims extra whitespace around the figure, and transparent=True, which makes the figure background transparent (useful for PNG overlays).

fig.savefig('publication_figure.pdf', bbox_inches='tight')   # clean PDF for a paper
fig.savefig('web_thumbnail.png', dpi=150, transparent=True)  # transparent PNG for web

Displaying a figure is done with plt.show(). In a standard Python script, this call is necessary to actually render and display the figure in a window; without it, the script will complete and exit without ever showing you anything. In Jupyter Notebooks, figures are often displayed automatically at the end of a cell due to Matplotlib's inline backend, but explicitly calling plt.show() is still good practice because it clarifies intent and ensures consistent behavior. An important ordering rule: always call plt.savefig() before plt.show(). The reason is that plt.show() in non-interactive backends finalizes and clears the current figure after displaying it, so calling it first leaves nothing to save.

plt.plot([1, 2, 3], [4, 5, 6])
plt.savefig('my_plot.png', dpi=200)   # save first
plt.show()                             # then display

Finally, releasing figure memory is done with plt.close(). This is a point that beginners frequently overlook, especially when writing scripts or loops that generate many figures. Matplotlib keeps figure objects in memory until they are explicitly closed or the Python process ends. If you are generating dozens or hundreds of figures in a loop — for example, creating one plot per data file in a batch processing workflow — and not closing them, memory usage will grow continuously and may eventually cause your program to crash or slow to a crawl:

import matplotlib.pyplot as plt
import numpy as np

for i in range(50):
    fig, ax = plt.subplots()
    ax.plot(np.random.randn(100))
    ax.set_title(f'Random Walk {i}')
    fig.savefig(f'random_walk_{i}.png', dpi=150)
    plt.close(fig)    # release this figure's memory before the next iteration

Passing the specific figure object to plt.close(fig) is the most precise approach, ensuring only that figure is closed. You can also call plt.close('all') to release all currently open figures at once, which is a useful cleanup step at the beginning or end of a script.

Taken together, these four areas — the library itself, the pyplot interface, the Figure/Axes object model, and the display/save workflow — form the conceptual foundation on which all Matplotlib work is built. Every more advanced topic, from custom colormaps to animated plots to embedding figures in GUI applications, builds directly on this foundation. Taking the time to understand these concepts thoroughly, rather than treating Matplotlib as a collection of magic incantations, pays enormous dividends as your visualization needs grow in complexity.

NotesThe hybrid pyplot/OO style shown in the subplots example is widely considered best practice for anything beyond a single simple plot. Students should be encouraged early to store references to fig and ax rather than relying exclusively on pyplot's state machine, as this habit avoids many common bugs in multi-axes or multi-figure code.