Matplotlib has been the default way to draw a picture of your data in Python for more than two decades. Its README describes it as a comprehensive library for creating static, animated, and interactive visualizations, one that produces publication-quality figures in a variety of hardcopy formats and interactive environments across platforms. You meet it as three friendly lines of code, but underneath sits a layered object system reconciling two hard requirements: pixel-perfect output in dozens of formats, and live interaction inside half a dozen GUI toolkits.
Reading the source is the fastest way to understand why the library looks the way it does from the outside. The design separates what you draw (Figure, Axes, and a tree of Artist objects) from how it is drawn (renderer objects supplied by pluggable backends), with a coordinate algebra (transforms) and a color algebra (Normalize plus Colormap) sandwiched in between. Once you see those seams, functions like plt.plot() stop being magic: they are thin forwarders that build objects and ask a backend to draw them.
This post walks the current main branch of the repository with two diagrams. The first gives the ten-minute overview of how the layers fit together. The second zooms into the classes and files where each responsibility actually lives, with links into the code so you can verify every claim yourself. All line numbers below were checked against the repository at the time of writing; drift is expected as the project evolves.
Overview of Matplotlib's architecture: the pyplot facade on the left delegates to the Figure and Axes object tree, artists carry color and transform state, and backend canvases turn that state into raster, vector, or animated output.
Reading the overview from left to right:
- The facade. Every script starts at pyplot, L904, where the module-level figure() factory lives. The plotting verbs you call every day are forwarders too: plot, L4042 delegates to the current Axes, and show, L602 hands control to the backend’s main loop. Convenience without hidden state would be impossible here, and pyplot manages exactly one piece of state: which figure and axes are “current.”
- The figure registry. That state lives in Gcf, L9, a tiny class in _pylab_helpers.py that keeps a stack of open figure managers. When pyplot needs to know “the current figure,” it asks Gcf; when you close a window, Gcf removes the entry. This is why plt.figure() returns a new Figure each time but plt.plot() targets the last one.
- Global configuration. Defaults for everything from figure size to font family flow through the rcParams, L1011 dictionary defined in the package init.py. The same file hosts use, L1229, the documented way to switch backends, and get_backend, L1305, which reports the resolved choice.
- The Figure. A Figure, L2511 is the root container of everything you see on one canvas. Its add_subplot() method at add_subplot, L737 creates and registers Axes children, and its savefig() implementation at savefig, L3440 is where “export to PNG, PDF, SVG, or EPS” begins.
- The Axes. The workhorse class is Axes, L89. It owns the data-to-artist conversion for every chart type: bar, L2308 builds Rectangle patches, hist, L7325 aggregates and draws, scatter, L5245 builds a PathCollection. It also hosts legend, L235, which assembles a Legend artist from the Axes’ labeled children.
- Artists. Everything that can appear on the canvas inherits from Artist, L153. The single draw() entry point at draw, L1089 is where a renderer walks the tree, and set_visible, L1150 is as simple as it sounds. Line2D, Text, Patch, and Collection are all Artists, which is what makes the tree uniform.
- Color. Data values become colors through two cooperating classes: Normalize, L2405 maps numbers onto the zero-to-one interval, and Colormap, L713 maps that interval onto RGBA tuples. The standalone helper to_rgba, L314 resolves every color string and alpha you pass by hand.
- Backends. The backend-neutral contract is FigureCanvasBase, L1792, which pairs a Figure with an event surface. Concrete implementations like FigureCanvasAgg, L492 (antialiased raster) and the SVG canvas with RendererSVG, L329 (vector) plug in from the backends subpackage.
- Animation. FuncAnimation, L1580 wraps the same object tree in a timer: it calls your update function each frame and asks the canvas to redraw, optionally streaming frames through a MovieWriter to produce a file.
The detail view adds the intermediate layers: GridSpec subplot placement, Axis with its Locator and Formatter partners, the container classes that group artists, the Colormap registry, the backend registry that resolves names at import time, and the Agg, SVG, and Qt canvas implementations.
One Library, Two APIs
The most important architectural fact about Matplotlib is that there are two APIs sharing one object tree. The pyplot module implements the MATLAB-style state machine that tutorials teach first, while the object-oriented API manipulates Figure and Axes directly. The source makes the relationship explicit: pyplot.plot() does nothing but look up the current Axes and call its plot() method at plot, L1549. The explicit style is what libraries should embed, because “current figure” state is a poor fit inside another application’s event loop.
The layering continues downward. The FigureBase, L183 class holds the subplot machinery shared by Figure, and each Axes inherits from _AxesBase, L558, which carries limit handling like set_xlim, L3855 and dual-axis tricks such as twinx, L4795. Below the Axes sits an Axis object per direction, Axis, L625 with XAxis and YAxis subclasses, which owns ticks. Tick placement and tick text are delegated to two strategy families: Locator, L1734 subclasses decide where ticks go, and Formatter, L199 subclasses decide how they read. Log and linear spacing come from scale classes such as LogScale, L381 in scale.py. This division of labor is why you can fix one tick formatter without touching anything else.
From Install to Your First Model
Matplotlib ships on PyPI and installs with a single command:
pip install matplotlib
Then the shortest complete visualization session, straight from the library’s own quick-start idiom:
import matplotlib.pyplot as plt
plt.plot([1, 2, 3, 4], [1, 4, 9, 16])
plt.ylabel("squares")
plt.show()
Here is what those five lines trigger inside the source. Importing pyplot selects and imports a backend automatically, the same resolution you can force explicitly with the use() function described above; in a plain script you get the Agg-backed desktop canvas, and FigureCanvasQTAgg at FigureCanvasQTAgg, L15 takes over inside Qt applications. plt.plot() asks Gcf for the current figure, creates a Figure with default size and dpi from rcParams, adds one Axes, and converts your two lists into a single Line2D artist at Line2D, L265. plt.show() then enters the backend’s event loop so the window stays open and interactive, driven by the canvas’s key and mouse handlers such as key_press_handler at key_press_handler, L2566.
In a headless script you would skip show() and call fig.savefig(“out.png”) instead, which routes through the canvas to RendererAgg at RendererAgg, L71 and writes a PNG buffer. If your data lives in a DataFrame rather than plain lists, plotting columns works the same way; our pandas and numpy tours cover the structures you would pass in.
Backends: One Contract, Many Surfaces
The backend system is the part of the design that ages best. A backend supplies two cooperating objects: a canvas, which owns the GUI or file surface and translates events, and a renderer, which knows how to draw primitives onto that surface. The Agg backend rasterizes into an antialiased pixel buffer and serves PNG output; the SVG backend emits vector elements; the PDF and PS backends do the same for print formats; Qt, Tk, GTK, and wx widgets embed a canvas for interactivity. Because every renderer implements the same drawing interface, a Figure is agnostic about its destination until the moment of output.
Backend selection itself is centralized in a small registry class, BackendRegistry, L15, which maps names to modules and applies filters during resolution. The ShowBase class at ShowBase, L3820 standardizes what “show the figure” means across toolkits. This is also why Matplotlib embeds so cleanly elsewhere: web applications built with frameworks like streamlit simply request PNG bytes from the Agg canvas and hand them to the browser, no GUI toolkit involved.
Color, Transforms, and Animation
Two quiet subsystems make every chart correct, and both are worth knowing when output looks wrong. The first is the transform tree. Every artist carries an Affine2D transform, Affine2D, L1941, and transforms compose: data coordinates flow through Axes and Figure transforms down to display pixels, with rectangles (Bbox, Bbox, L692) describing each region. Blended transforms like BlendedGenericTransform, L2231 are what let an annotation stay fixed in x while tracking data in y. When a legend overlaps your data or text lands off-canvas, the answer is almost always in this tree, and the Transform base class at Transform, L1328 documents the composition rules.
The second subsystem is color. Named colormaps live in a registry exposed in cm.py, ColormapRegistry, L66, and the modern Colorizer class at Colorizer, L38 pairs a normalization with a colormap for artists that encode data as color, such as the PathCollection behind scatter plots. Animation rounds out the feature set: the Animation base at Animation, L859 and its timed subclass drive repeated redraws, and MovieWriter at MovieWriter, L233 captures the frames for GIF or MP4 export.
Honest Limits
Staying with what the project itself documents: Matplotlib is a visualization library, not a data analysis toolkit. It will not clean your data, fit your model, or hold your tables; it draws what you hand it. Its README’s own framing, publication-quality figures across hardcopy formats and interactive environments, is accurate, but “interactive” is conditional: an interactive backend needs a supported GUI toolkit or browser environment installed, and in headless settings you are limited to file output. Installation details are deliberately left to the official install documentation rather than the README, and complex layouts can require understanding the transform and layout machinery rather than a single keyword argument. Finally, line numbers in this post were verified against the main branch at publication time and will drift; the diagrams link to files rather than frozen snapshots so the targets stay findable.
Where Matplotlib Sits Next to the Rest of the Stack
Matplotlib’s persistence is not nostalgia. The artist tree is a clean intermediate representation, and the backend contract makes it at home in scripts, notebooks, desktop apps, and web servers alike. If you are charting data from polars frames, publishing figures into a report, or embedding live plots in a tool, the mental model from this tour, facade to Figure to Artists to renderer, will map directly onto the error messages and extension points you meet. The lesson generalizes to every library in this series: the durable part is the contract at its center, and in Matplotlib that contract is a tree of Artists asking a backend to draw. Enjoyed this post? Never miss out on future posts by following us