Matplotlib vs Seaborn: Which Should You Plot With?
Choose seaborn for fast, good-looking statistical plots from DataFrames; choose matplotlib when you need pixel-level control over every element or chart types seaborn doesn't cover. It's not really either/or: seaborn is built on top of matplotlib, so you'll almost always use both in the same figure.
Matplotlib vs Seaborn at a Glance
Seaborn trades control for convenience: one function call replaces a dozen lines of matplotlib, but matplotlib remains the layer underneath that you drop into for fine-tuning.
Factor | matplotlib | seaborn
------------------|--------------------------|---------------------------
Level | Low-level, general | High-level, statistical
Input | Arrays, lists, anything | Tidy pandas DataFrames
Default look | Plain, dated | Polished themes/palettes
Stats built in | No | Yes (CI bands, KDE, regr.)
Grouped plots | Manual loops | hue= / col= / row= params
Faceting | Manual subplots | One line (catplot/relplot)
Custom control | Total | Via matplotlib underneath
Chart coverage | Everything | Statistical charts only
What Does "Low-Level Control" Actually Mean?
Matplotlib exposes every figure element โ axes, ticks, spines, annotations, exact positions โ which makes it the tool for publication figures, custom layouts, and unusual chart types. The cost is verbosity: grouping by a category means looping and coloring manually, and the defaults need styling work before a chart looks presentable.
What Do Seaborn's Statistical Defaults Buy You?
Seaborn functions understand DataFrames and statistics natively: pass hue="segment" and it splits, colors, and adds a legend automatically; sns.lmplot fits and draws a regression with confidence bands; sns.histplot(kde=True) overlays a density estimate. What takes 15 lines of matplotlib is typically one seaborn call.
The Same Plot in Both Libraries
Here's a scatter plot colored by category โ the classic case where seaborn saves the most code.
import matplotlib.pyplot as plt
import seaborn as sns
# df has columns: total_bill, tip, day
df = sns.load_dataset("tips")
# matplotlib โ manual grouping and legend
fig, ax = plt.subplots()
for day, grp in df.groupby("day", observed=True):
ax.scatter(grp["total_bill"], grp["tip"], label=day)
ax.set_xlabel("total_bill")
ax.set_ylabel("tip")
ax.legend(title="day")
# seaborn โ one line, same result, nicer defaults
sns.scatterplot(data=df, x="total_bill", y="tip", hue="day")
How Do You Use Them Together?
Every seaborn axes-level function returns (or draws onto) a matplotlib Axes, so the standard workflow is: seaborn for the plot, matplotlib for the finish.
fig, ax = plt.subplots(figsize=(8, 4))
sns.boxplot(data=df, x="day", y="total_bill", ax=ax)
ax.set_title("Bills by Day") # matplotlib touch-ups
ax.axhline(20, ls="--", color="gray") # reference line
fig.savefig("bills.png", dpi=150, bbox_inches="tight")
This pattern โ ax=ax in, matplotlib methods out โ covers nearly every real-world chart.
Which Should You Choose?
Learn both, in this order: enough matplotlib to understand figures, axes, and plt.subplots(), then seaborn for day-to-day exploratory work. Reach for pure matplotlib when you need non-statistical charts, precise publication layouts, or custom annotations; reach for seaborn whenever your data is a tidy DataFrame and the chart is statistical.
Common Pitfalls
- Skipping matplotlib basics: you can't fix a seaborn chart's title, limits, or size without knowing the
AxesAPI underneath. - Figure-level vs axes-level confusion:
sns.catplot/relplotcreate their own figure and ignoreax=; useboxplot/scatterplotetc. when composing subplots. - Untidy data: seaborn expects long/tidy format. Wide data usually needs
df.melt()first. - Styling by hand: set
sns.set_theme()once instead of styling each chart individually โ it restyles plain matplotlib output too.
Pro Tip: Put sns.set_theme() at the top of every notebook even if you never call another seaborn function โ it upgrades matplotlib's default fonts, grid, and colors for free.