Data without visualisation is just numbers. The moment you put a trend on a chart or show the distribution of a variable in a histogram, patterns become obvious that were invisible in a spreadsheet. Matplotlib is the library that makes this possible in Python — and it is the foundation that every other Python visualisation tool is built on.
Seaborn produces beautiful statistical charts in a few lines of code, but when something looks wrong or needs custom styling, you need Matplotlib to fix it. Pandas plotting is convenient for quick exploration, but it is Matplotlib under the hood. Even Plotly’s Python API shares design concepts rooted in Matplotlib’s figure-and-axes model. Learning Matplotlib once gives you leverage across the entire Python visualisation ecosystem.
This guide moves from fundamentals to production-quality output, covering the chart types you will use most often and the customisation techniques that separate amateur plots from professional figures.
The Figure and Axes Model
Before writing any code, it is worth understanding Matplotlib’s architecture. New users often get confused between two styles: the quick plt.plot() style and the object-oriented fig, ax = plt.subplots() style. Both work, but they represent different mental models.
In Matplotlib, a Figure is the entire canvas — the white rectangle that contains everything. An Axes is a single chart area within that figure, with its own x-axis, y-axis, title, and data series. A figure can contain one axes or many.
The plt.plot() style (sometimes called “pyplot style”) is fine for quick single charts in a notebook. The fig, ax object-oriented style is better for everything else: multiple charts, fine-grained customisation, and reusable functions. This guide uses the object-oriented style throughout, which is the right habit to build.
pip install matplotlib numpy pandas seaborn
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
# Set a clean, professional default style
plt.style.use('seaborn-v0_8-whitegrid')
plt.rcParams.update({
'font.size': 12,
'axes.titlesize': 14,
'axes.titleweight': 'bold',
'figure.dpi': 100
})
Line Charts: Showing Trends Over Time
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
traffic = [12400, 13100, 15800, 16200, 18900, 21300,
19800, 22100, 25600, 27300, 24800, 29100]
revenue = [48000, 51000, 61000, 63000, 73000, 82000,
77000, 86000, 99000, 106000, 96000, 113000]
fig, ax1 = plt.subplots(figsize=(11, 5))
# Primary axis: traffic
color_traffic = 'steelblue'
ax1.plot(months, traffic, color=color_traffic, linewidth=2.5,
marker='o', markersize=5, label='Monthly Traffic')
ax1.set_xlabel('Month 2026', fontsize=12)
ax1.set_ylabel('Website Visitors', color=color_traffic, fontsize=12)
ax1.tick_params(axis='y', labelcolor=color_traffic)
# Secondary axis: revenue
ax2 = ax1.twinx()
color_revenue = 'coral'
ax2.plot(months, revenue, color=color_revenue, linewidth=2.5,
marker='s', markersize=5, linestyle='--', label='Revenue (₹)')
ax2.set_ylabel('Revenue (₹)', color=color_revenue, fontsize=12)
ax2.tick_params(axis='y', labelcolor=color_revenue)
ax1.set_title('DataExpertise.in — Traffic and Revenue 2026')
# Combine legends from both axes
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')
plt.tight_layout()
plt.show()
Bar Charts: Comparing Categories
languages = ['Python', 'SQL', 'R', 'Scala', 'Julia']
usage = [85, 72, 45, 28, 15]
# Highlight the top value
colors = ['steelblue' if u == max(usage) else '#B0C4DE' for u in usage]
fig, ax = plt.subplots(figsize=(9, 5))
bars = ax.bar(languages, usage, color=colors, edgecolor='white',
width=0.6, zorder=2)
# Add value labels on top of each bar
for bar, val in zip(bars, usage):
ax.text(bar.get_x() + bar.get_width() / 2,
bar.get_height() + 1,
f'{val}%',
ha='center', va='bottom',
fontsize=11, fontweight='bold', color='#333333')
ax.set_title('Most Used Data Science Languages — Industry Survey 2026')
ax.set_ylabel('Usage Percentage (%)')
ax.set_ylim(0, 100)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.grid(axis='y', alpha=0.4, zorder=1)
plt.tight_layout()
plt.show()
Scatter Plots: Finding Relationships
np.random.seed(42)
experience = np.random.uniform(0, 15, 300)
salary = 300000 + 80000 * experience + np.random.normal(0, 150000, 300)
city_tier = np.random.choice([1, 2, 3], 300, p=[0.3, 0.4, 0.3])
colors_city = {1: 'steelblue', 2: 'coral', 3: 'mediumseagreen'}
labels_city = {1: 'Tier-1 City', 2: 'Tier-2 City', 3: 'Tier-3 City'}
fig, ax = plt.subplots(figsize=(9, 6))
for tier in [1, 2, 3]:
mask = city_tier == tier
ax.scatter(experience[mask], salary[mask],
c=colors_city[tier], label=labels_city[tier],
alpha=0.5, s=45, edgecolors='none')
# Trend line
z = np.polyfit(experience, salary, 1)
p = np.poly1d(z)
x_line = np.linspace(0, 15, 100)
ax.plot(x_line, p(x_line), 'k--', linewidth=1.5, alpha=0.6, label='Trend')
ax.set_xlabel('Years of Experience', fontsize=12)
ax.set_ylabel('Annual Salary (₹)', fontsize=12)
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'₹{x/100000:.0f}L'))
ax.set_title('Data Scientist Salary vs Experience in India 2026')
ax.legend(fontsize=10)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.show()
Histograms: Understanding Distributions
np.random.seed(42)
# Bimodal salary distribution — two distinct populations
salaries_junior = np.random.normal(600000, 150000, 300)
salaries_senior = np.random.normal(1800000, 400000, 200)
all_salaries = np.concatenate([salaries_junior, salaries_senior])
fig, ax = plt.subplots(figsize=(10, 5))
ax.hist(all_salaries, bins=40, color='steelblue',
edgecolor='white', alpha=0.8, density=True)
ax.axvline(np.median(all_salaries), color='red', linewidth=2,
linestyle='--', label=f'Median: ₹{np.median(all_salaries)/100000:.1f}L')
ax.axvline(np.mean(all_salaries), color='orange', linewidth=2,
linestyle='--', label=f'Mean: ₹{np.mean(all_salaries)/100000:.1f}L')
ax.set_xlabel('Annual Salary (₹)', fontsize=12)
ax.set_ylabel('Density')
ax.set_title('Data Scientist Salary Distribution India 2026')
ax.xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'₹{x/100000:.0f}L'))
ax.legend(fontsize=11)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.show()
Multi-Chart Dashboards with Subplots
fig, axes = plt.subplots(2, 2, figsize=(14, 9))
fig.suptitle('Data Science Blog Analytics Dashboard',
fontsize=16, fontweight='bold', y=1.01)
# Top-left: Monthly traffic trend
x = np.arange(12)
axes[0, 0].fill_between(x, traffic, alpha=0.3, color='steelblue')
axes[0, 0].plot(x, traffic, 'o-', color='steelblue', linewidth=2, markersize=4)
axes[0, 0].set_xticks(x); axes[0, 0].set_xticklabels(months, rotation=45)
axes[0, 0].set_title('Monthly Traffic')
# Top-right: Category breakdown
cats = ['ML', 'Python', 'SQL', 'Viz', 'Career']
vals = [132, 45, 41, 26, 15]
axes[0, 1].barh(cats, vals, color='coral', edgecolor='white')
axes[0, 1].set_title('Posts by Category')
# Bottom-left: Revenue growth
axes[1, 0].bar(months, [r/1000 for r in revenue],
color='mediumseagreen', edgecolor='white')
axes[1, 0].set_xticklabels(months, rotation=45)
axes[1, 0].set_title('Monthly Revenue (₹ thousands)')
# Bottom-right: Traffic source pie
sizes = [45, 30, 15, 10]
sources = ['Organic Search', 'Direct', 'Social Media', 'Referral']
axes[1, 1].pie(sizes, labels=sources, autopct='%1.0f%%',
colors=['steelblue', 'coral', 'mediumseagreen', '#B0C4DE'],
startangle=90)
axes[1, 1].set_title('Traffic Sources')
for ax in axes.flat:
if not ax.get_title() == 'Traffic Sources':
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.show()
Saving High-Quality Figures
# Always savefig BEFORE plt.show() — show() clears the figure
plt.savefig('dashboard.png', dpi=150, bbox_inches='tight') # Web/presentations
plt.savefig('dashboard.pdf', bbox_inches='tight') # Reports (vector)
plt.savefig('dashboard.svg', bbox_inches='tight') # Web (scalable)
plt.show()
Frequently Asked Questions
Matplotlib vs Seaborn — which should I learn first, and when should I switch?
Learn Matplotlib first, without exception. Seaborn is built directly on Matplotlib and produces beautiful statistical charts with much less code — but when something breaks, looks wrong, or needs custom styling, you will need Matplotlib to fix it. Someone who knows only Seaborn is stuck the moment they need to adjust axis tick spacing, add a custom annotation, or combine a Seaborn plot with a Matplotlib one. Spend one to two weeks with Matplotlib building the six fundamental chart types. After that, use Seaborn for statistical charts (heatmaps, violin plots, pairplots, regression plots) since it handles those far more elegantly, and drop back to Matplotlib for customisation and control.
Why does plt.show() sometimes produce a blank figure, or savefig() save an empty image?
The most common cause: calling plt.show() before plt.savefig(). The show() function renders the figure and then clears it from memory. If you save after showing, you save a blank canvas. Always call savefig() first, then show(). A second common cause: calling savefig() with the wrong file path, or without bbox_inches=’tight’, which can clip axis labels from the saved file. A third issue: in some environments, calling plt.close() after show() is necessary to prevent figures from stacking up in memory during loops.
How do I make my Matplotlib charts look more professional?
Professional charts follow three principles: remove clutter, use intentional colour, and guide attention. For clutter: remove the top and right spines (ax.spines[‘top’].set_visible(False)), use a white or very light grey background, and minimise grid lines. For colour: use one prominent colour for the key data series and muted tones for supporting series. Avoid rainbow colour schemes. For attention: add a descriptive title that states the insight (not just the variable names), add source annotations, and use direct labels rather than legends where possible. The seaborn-v0_8-whitegrid style is an excellent starting point that handles most of this automatically.
How do I create interactive charts in Python?
Matplotlib produces static images. For interactive charts — where users can zoom, pan, hover for tooltips, or filter data — you need a different library. Plotly is the most popular choice for both web and notebook interactivity. Bokeh is excellent for web-embedded dashboards. Altair provides a clean, declarative syntax for interactive charts. If you are building internal data tools or dashboards, Plotly Dash or Streamlit let you build full interactive web apps in pure Python without writing JavaScript. In 2026, Plotly Express is the most common choice for quick interactive charts in notebooks due to its similarity to Matplotlib syntax.



