Introduction
If you’re working in data science, machine learning, scientific computing, or any flavour of
analysis where you need to see your data, Matplotlib is the foundational plotting library to
learn. It’s not the prettiest by default and it isn’t the most modern API in Python, but
it’s the one that runs underneath most of the others (Seaborn, Pandas .plot()) and
the one you’ll see in nearly every real codebase.
This guide takes you from zero to comfortable: the mental model, the architecture, every chart type worth knowing, the customisations that make plots look like something you’d ship, and how to integrate with NumPy and Pandas. By the end you’ll be able to look at a chart in someone’s notebook and reproduce it.
📚 Table of contents
- What Matplotlib is and why it’s still the default
- Setup — install, Jupyter notebook, imports
- The Figure / Axes / Axis mental model
- Line plots — the simplest case
- Object-oriented vs PLT global style
- Scatter plots with colour maps
- Bar charts, grouped bars, and horizontal bars
- Histograms and overlapping distributions
- Subplots and tight layouts
- Plotting from Pandas DataFrames
- Advanced customisation — annotations, vertical lines, text boxes
- Themes, styles, and pie charts
- Saving figures to PNG / PDF / SVG
- Frequently asked questions
📊 What Matplotlib is
Matplotlib is a comprehensive library for creating static and interactive visualisations in Python, with a MATLAB-like interface for plotting data. It pairs constantly with NumPy and Pandas — NumPy provides the numerical arrays you’re plotting, Pandas the DataFrames, and Matplotlib draws the picture.
Strengths: massive flexibility, supports nearly every chart type, infinite customisation, runs
everywhere. Trade-offs: the API is sprawling, defaults are dated, and you’ll spend the first few
sessions getting used to the dual interface (state-based plt.… and object-oriented
fig, ax).
🛠️ Setup
Install Matplotlib along with NumPy and Pandas:
pip install matplotlib numpy pandas # Windows
pip3 install matplotlib numpy pandas # macOS / Linux
The easiest place to play with plots is a Jupyter notebook. In VS Code or Cursor, hit Ctrl/Cmd + Shift + P, type Jupyter: Create New Notebook, and pick a Python kernel. Cells run inline, plots render under the cell.
Standard imports for any Matplotlib work:
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
%matplotlib inline # auto-show plots in Jupyter
plt.style.use('default') # or 'seaborn-v0_8', 'ggplot', 'dark_background', etc.
🏛️ The Figure / Axes / Axis mental model
Three terms that sound alike and confuse everyone:
Figure
The top-level container — the whole window or page that holds your plots. Think canvas.
Axes
A single plot region inside the figure. A figure can hold many. Think individual painting on the canvas.
Axis
A single dimension on a plot — the x-axis, the y-axis. Think the rulers on each painting.
📈 Line plots — the simplest case
x = [1, 2, 3, 4, 5]
y = [2, 4, 6, 8, 10]
plt.plot(x, y)
plt.xlabel("X axis")
plt.ylabel("Y axis")
plt.title("My first plot")
plt.show()
In a notebook you can skip plt.show(); the cell auto-renders. In a regular Python script
you need it to actually display the window. Multiple plt.plot() calls before
plt.show() overlay lines on the same axes.
🧱 Object-oriented vs PLT global
Two equivalent ways to make the same plot:
# State-based (PLT global)
plt.plot(x, y)
plt.title("Hello")
# Object-oriented (recommended for complex plots)
fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_title("Hello")
The object-oriented style scales better when you have multiple subplots or want explicit control over which axes you’re drawing on. Use it for anything beyond a single quick chart.
⚫ Scatter plots with colour maps
n = 100
x = np.random.rand(n)
y = np.random.rand(n)
colors = np.random.rand(n) # 0..1 mapped through a colour map
sizes = 1000 * np.random.rand(n) # marker sizes in points^2
plt.figure(figsize=(8, 5))
plt.scatter(x, y, c=colors, s=sizes, alpha=0.6, cmap='viridis')
plt.colorbar()
plt.show()
Useful colour maps: viridis (perceptually uniform default), plasma,
magma, coolwarm, Set1 for categorical. Pass alpha
less than 1 to make overlapping points visible.
📊 Bar charts, grouped bars, horizontal bars
Basic vertical bar:
categories = ['A', 'B', 'C', 'D']
values = [10, 24, 17, 32]
plt.bar(categories, values, color='steelblue', edgecolor='black', linewidth=1)
Horizontal? Swap bar for barh and everything else stays the same.
Grouped bars (e.g. product A/B/C across Q1–Q4) need a little arithmetic on the x-axis to shift each group’s bars left or right:
categories = ['Q1', 'Q2', 'Q3', 'Q4']
x = np.arange(len(categories))
width = 0.25
plt.bar(x - width, product_a, width, label='A')
plt.bar(x, product_b, width, label='B')
plt.bar(x + width, product_c, width, label='C')
plt.xticks(x, categories) # show categorical labels at the centre positions
plt.legend()
Mental model: x is the centre, you shift the left bar by -width and the right
by +width, then re-label the ticks so they read as Q1–Q4.
📉 Histograms & overlapping distributions
data = np.random.randn(1000)
plt.hist(data, bins=30, color='steelblue', edgecolor='black')
# Multiple distributions overlaid
plt.hist(data1, bins=30, alpha=0.5, label='Group 1')
plt.hist(data2, bins=30, alpha=0.5, label='Group 2')
plt.legend()
With alpha<1, overlapping regions blend visually — useful to compare
distributions. With alpha=1, the second histogram completely covers the first wherever
they overlap, which is occasionally what you want.
🧩 Subplots
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
axes[0, 0].plot(x, np.sin(x))
axes[0, 1].plot(x, np.cos(x))
axes[1, 0].scatter(np.random.rand(50), np.random.rand(50))
axes[1, 1].bar(['A', 'B', 'C'], [3, 7, 5])
plt.tight_layout()
plt.show()
Index axes like a 2D array: [0, 0] top-left, [1, 1] bottom-right.
tight_layout() compacts the spacing so subplots don’t bleed into each other.
🐼 Plotting from Pandas DataFrames
Two paths:
- Pass DataFrame columns into Matplotlib directly:
plt.plot(df['date'], df['sales']). - Use
df.plot():df.plot(x='date', y='sales', ax=ax)— cleaner when you have many columns and want quick visuals.
Both produce real Matplotlib axes you can keep customising afterwards.
🎯 Advanced — annotations, vertical lines, text boxes
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(x, y, 'b-', linewidth=2)
# Annotation with arrow pointing at a peak
peak = np.argmax(y)
ax.annotate('Maximum',
xy=(x[peak], y[peak]),
xytext=(x[peak] + 1, y[peak] - 0.3),
arrowprops=dict(arrowstyle='->', color='red', lw=2),
fontsize=12, fontweight='bold')
# Text box anchored to axes coords
ax.text(0.05, 0.95, 'Sine wave',
transform=ax.transAxes,
fontsize=11,
verticalalignment='top',
bbox=dict(boxstyle='round', facecolor='wheat'))
# Vertical and horizontal reference lines
ax.axvline(x=5, color='grey', linestyle='--')
ax.axhline(y=0, color='grey', linestyle=':')
annotate() draws an arrow from xytext to xy. axvline
and axhline draw reference lines across the whole plot — perfect for marking
thresholds, current dates, or boundaries.
🎨 Themes, styles, pie charts
Try different themes:
print(plt.style.available)
plt.style.use('ggplot') # or 'seaborn-v0_8', 'dark_background', 'fivethirtyeight'
Pie chart with an exploded slice:
sizes = [30, 25, 20, 15, 10]
labels = ['A', 'B', 'C', 'D', 'E']
explode = (0.1, 0, 0, 0, 0) # pop out the first slice
plt.pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%',
shadow=True, startangle=90)
plt.show()
💾 Saving figures
plt.savefig('chart.png', dpi=300, bbox_inches='tight')
plt.savefig('chart.pdf') # vector format, scales cleanly
plt.savefig('chart.svg') # for the web
Use PNG for slides, PDF or SVG when scale matters. bbox_inches='tight' trims excess
whitespace; without it your saved chart will have padding the notebook hides.
✨ Best practices & common mistakes
✅ Do
- Use the object-oriented API for anything non-trivial.
- Always label axes and add a title.
- Call
tight_layout()before showing or saving. - Use
alphawhen points overlap.
❌ Don’t
- Mix state-based and object-oriented styles in the same plot.
- Forget
plt.show()in scripts (notebooks auto-render). - Use red/green for categorical contrast — accessibility-hostile.
- Hard-code dpi without checking resolution at output size.
Explore More on DevShelf
-
Defensive Python: Edge Cases and Validation
The next Python skill after visualization — write code that doesn't blow up on bad data before it reaches your plots.
-
Famous CS Algorithms Explained
Pair visual intuition with algorithmic thinking — the sorting and graph algorithms that Matplotlib is often used to illustrate.