Home Knowledge Base Matplotlib's object model separates the act of building a visualization from the act of rendering it: every call to `ax.plot()`, `ax.scatter()`, or `ax.text()` creates an Artist object and adds it to the Axes container, and no pixels are produced until `fig.savefig()` or `plt.show()` triggers a renderer pass over the full hierarchy.

Matplotlib is a Python plotting library that organizes every visualization as a hierarchy of Artist objects—Figure containing one or more Axes, each Axes containing Line2D, Patch, Text, and Collection artists—and renders that hierarchy to a pixel buffer or vector format by dispatching to a pluggable backend, with the Agg rasterizer (Anti-Grain Geometry) producing PNG output and the Cairo backend producing PDF, SVG, and PostScript.

<svg viewBox="0 0 760 470" xmlns="http://www.w3.org/2000/svg" font-family="ui-monospace,monospace">
  <rect width="760" height="470" fill="#0d1117"/>
  <text x="380" y="32" text-anchor="middle" fill="#e6edf3" font-size="21" font-weight="700">Matplotlib Artist Hierarchy</text>
  <text x="380" y="52" text-anchor="middle" fill="#8b98a5" font-size="13">Figure → Axes → Artists — every drawn element is an Artist with style properties</text>

  <!-- Figure boundary -->
  <rect x="30" y="68" width="700" height="310" rx="6" fill="#161b22" stroke="#1f6feb" stroke-width="2"/>
  <text x="46" y="88" fill="#58a6ff" font-size="12" font-weight="700">Figure  (canvas, DPI, size)</text>
  <text x="46" y="104" fill="#6e7681" font-size="10">fig = plt.figure(figsize=(8,6), dpi=150)  →  1200×900 px raster</text>

  <!-- Axes 1 -->
  <rect x="50" y="112" width="300" height="240" rx="4" fill="#0d1117" stroke="#238636" stroke-width="1.5"/>
  <text x="66" y="130" fill="#3fb950" font-size="11" font-weight="700">Axes  (coordinate system + spine + tick)</text>

  <!-- mock line plot inside axes 1 -->
  <polyline points="70,290 100,260 140,240 190,220 240,200 290,230 330,210" fill="none" stroke="#58a6ff" stroke-width="2"/>
  <text x="66" y="200" fill="#6e7681" font-size="9">Line2D artist</text>
  <!-- x axis label -->
  <text x="200" y="330" text-anchor="middle" fill="#8b98a5" font-size="10">xlabel (Text artist)</text>
  <!-- y axis tick -->
  <text x="66" y="310" fill="#8b98a5" font-size="10">YTick (Line2D + Text)</text>

  <!-- Axes 2 -->
  <rect x="380" y="112" width="320" height="240" rx="4" fill="#0d1117" stroke="#8957e5" stroke-width="1.5"/>
  <text x="396" y="130" fill="#a371f7" font-size="11" font-weight="700">Axes  (independent coords)</text>

  <!-- mock scatter inside axes 2 -->
  <circle cx="430" cy="220" r="5" fill="#f0883e" fill-opacity="0.7"/>
  <circle cx="460" cy="200" r="7" fill="#f0883e" fill-opacity="0.7"/>
  <circle cx="500" cy="240" r="4" fill="#3fb950" fill-opacity="0.7"/>
  <circle cx="540" cy="210" r="9" fill="#1f6feb" fill-opacity="0.7"/>
  <circle cx="580" cy="230" r="6" fill="#f85149" fill-opacity="0.7"/>
  <circle cx="620" cy="190" r="5" fill="#f0883e" fill-opacity="0.7"/>
  <circle cx="660" cy="260" r="8" fill="#3fb950" fill-opacity="0.7"/>
  <text x="540" y="316" text-anchor="middle" fill="#8b98a5" font-size="10">PathCollection artist (scatter)</text>
  <text x="396" y="340" fill="#6e7681" font-size="9">each point = 1 Path  →  1M pts = 8 s render</text>

  <!-- Backend dispatch -->
  <rect x="30" y="392" width="700" height="60" rx="4" fill="#161b22" stroke="#30363d" stroke-width="1"/>
  <text x="380" y="412" text-anchor="middle" fill="#8b98a5" font-size="11">Backend dispatch  —  savefig() routes to renderer</text>
  <text x="100" y="432" text-anchor="middle" fill="#3fb950" font-size="10">Agg → PNG/JPEG</text>
  <text x="100" y="446" text-anchor="middle" fill="#6e7681" font-size="9">raster, ~180 KB</text>
  <text x="280" y="432" text-anchor="middle" fill="#58a6ff" font-size="10">Cairo → PDF/SVG/PS</text>
  <text x="280" y="446" text-anchor="middle" fill="#6e7681" font-size="9">vector, ~45 KB SVG</text>
  <text x="460" y="432" text-anchor="middle" fill="#a371f7" font-size="10">TkAgg/Qt5Agg → GUI window</text>
  <text x="460" y="446" text-anchor="middle" fill="#6e7681" font-size="9">interactive, pan/zoom</text>
  <text x="640" y="432" text-anchor="middle" fill="#f0883e" font-size="10">pgf → LaTeX/PGF</text>
  <text x="640" y="446" text-anchor="middle" fill="#6e7681" font-size="9">publication-quality</text>

  <text x="380" y="465" text-anchor="middle" fill="#6e7681" font-size="11">plt.close(fig) mandatory — circular Artist refs prevent Python GC from releasing figure memory</text>
</svg>

Matplotlib's object model separates the act of building a visualization from the act of rendering it: every call to ax.plot(), ax.scatter(), or ax.text() creates an Artist object and adds it to the Axes container, and no pixels are produced until fig.savefig() or plt.show() triggers a renderer pass over the full hierarchy. This deferred rendering means the same Figure object can be sent to multiple backends—Agg for a 1,200 × 900 PNG at 150 DPI, Cairo for a scalable SVG at 45 KB, or a GUI TkAgg window for interactive pan and zoom—without rebuilding any artists. The tradeoff is that every property change (color, linewidth, alpha) before savefig is free, but changes after rendering require a full re-render; animations work by mutating artist properties between frames rather than rebuilding the Figure.

Figure memory is managed through explicit close calls, not Python's garbage collector, because matplotlib maintains internal global state that creates reference cycles among Figure, Axes, and Canvas objects. Calling plt.savefig() without plt.close() in a loop that generates 1,000 figures accumulates all of them in memory; at roughly 2 MB per Figure (8 × 6 in, 100 DPI, Agg), this produces a 2 GB leak invisible to del fig. The correct pattern is plt.close(fig) immediately after saving, or using matplotlib.use('Agg') and the Figure class constructor directly rather than the plt state machine, which registers every Figure in a global _pylab_helpers.Gcf dictionary.

The pyplot state machine (plt.plot, plt.xlabel) is a convenience layer over the object-oriented API that automatically creates and tracks the current Figure and Axes—but it becomes a source of subtle bugs in code that creates multiple figures or runs in parallel. Every plt.plot() call dispatches to plt.gca().plot() (get-current-axes), where "current" is a global variable updated by plt.figure(), plt.subplot(), and plt.axes(). In a Jupyter notebook that re-executes cells out of order, or in a threaded server rendering multiple requests concurrently, the global current-axes pointer can be wrong. The object-oriented pattern—fig, ax = plt.subplots() followed by ax.plot()—eliminates this ambiguity entirely; every operation targets an explicit Axes object.

Scatter plots with more than 100,000 points are the most common matplotlib performance trap because plt.scatter renders each point as a separate Path artist, and 1,000,000 points take approximately 8 seconds to render via the Agg backend. The PathCollection artist underlying scatter stores one Path per marker, making per-point color and size trivially addressable but rendering cost O(N) in Python artist traversal before any rasterization. For large-N scatter, the alternatives are ax.plot(x, y, '.', markersize=1) which renders all points as a single Line2D artist at ~300 ms, or Datashader which rasterizes 1,000,000 points in ~160 ms by aggregating into a fixed-resolution pixel grid without individual-point artists at all—a 50× speedup over plt.scatter.

The rcParams system gives matplotlib approximately 200 configurable style parameters—figure size, DPI, font family, line width, color cycle, tick direction, spine visibility—that can be overridden globally via matplotlib.rcParams, per-session via plt.style.use(), or per-block via the plt.rc_context() context manager. A call to plt.style.use('seaborn-v0_8') atomically overrides roughly 40 rcParams to match Seaborn's aesthetics, while plt.rcParams['axes.spines.top'] = False surgically removes a single element. The matplotlibrc file (typically at ~/.config/matplotlib/) applies defaults to every matplotlib session without any in-code configuration—the canonical way to enforce consistent figure size and font family across a project.

FuncAnimation renders each frame by calling an update function that mutates existing artists, then rasterizes the result via the Agg backend and hands the pixel buffer to ffmpeg for encoding. A 100-frame animation at 30 fps produces a 3.3-second video; frame generation cost is dominated by the number of artists updated per frame, not the resolution—updating a single Line2D's data array with set_data() costs ~0.5 ms, while clearing and redrawing 1,000 artists costs ~50 ms, capping the animation at 20 fps before encoding even begins. The encoded MP4 from ffmpeg at quality CRF 23 typically reaches ~800 KB for a 3-second 1280×720 clip. Using blit=True instructs FuncAnimation to re-render only the bounding boxes of changed artists rather than the entire Figure, reducing per-frame cost by 3–10× for typical animations with a static background.

BackendOutput formatFile size (8×6 in plot)Use case
AggPNG, JPEG~180 KB PNGReports, notebooks, CI
CairoPDF, SVG, PS~45 KB SVGPublications, web
TkAgg / Qt5AggGUI windowInteractive exploration
pgfLaTeX PGF/TikZAcademic papers
MATPLOTLIB RENDER FLOWCHART

  ax.plot() / ax.scatter() / ax.text()
         │
         ▼
  ┌─────────────────────┐
  │  Artist created     │  Line2D / PathCollection / Text
  │  added to Axes      │  no pixels yet — deferred render
  └────────┬────────────┘
           │
           ▼
  ┌─────────────────────┐
  │  fig.savefig() or   │  triggers renderer pass
  │  plt.show()         │  traverses Artist hierarchy
  └────────┬────────────┘
           │
     ┌─────┴──────┐
     │            │
  Agg backend  Cairo/pgf
     │            │
  Rasterize    Vectorize
  to px buffer  to commands
     │            │
  PNG/JPEG     SVG/PDF/PS
     │
  plt.close(fig)  ← mandatory: clears Gcf registry

Read matplotlib through a deferred Artist hierarchy lens rather than a draw-commands lens. Every ax.plot() call is an instruction to build an object, not to draw a line; the renderer that actually produces pixels or vectors is invoked only at save-time and knows nothing about how artists were constructed. That separation is why rcParams, style sheets, and backend swaps can all modify the final output without touching any plot code—and why a plt.close() after every savefig() is not optional hygiene but a required cleanup of the persistent object graph that matplotlib maintains across the entire session. It also explains why Seaborn, Plotly, and Altair coexist with matplotlib rather than replacing it: they operate at a higher abstraction level (statistical encoding, grammar of graphics) but ultimately produce matplotlib Artists or independent scene graphs—the deferred hierarchy remains the universal lowest-common-denominator rendering contract in the Python visualization ecosystem.

matplotlibpyplotmatplotlib figurematplotlib axesmatplotlib animationmatplotlib scattermatplotlib backendsmatplotlib rcparamsmatplotlib stylepython visualizationmatplotlib savefigmatplotlib subplots

Explore 500+ Semiconductor & AI Topics

From EUV lithography to CUDA optimization — search the full knowledge base or chat with our AI assistant.