Skip to content
Back to Resources
Comparison

Python Data Visualization Libraries: 2026 Comparison

Skopx Team
July 30, 2026
15 min read

A reviewer leaves one comment on your pull request: "can we see this as a chart?" You have a dataframe, twenty minutes, and six python data visualization libraries already sitting in the same virtualenv. The wrong pick does not fail loudly. It fails three weeks later, when the chart has to go into a PDF and your library only emits HTML, or when the notebook that rendered beautifully in JupyterLab ships to a Flask app and renders nothing at all.

That is the real decision. Not "which library is prettiest," but "where does this chart end up, and what does the library produce." Everything else, including the endless syntax debates, follows from that one answer.

This is a comparison of the six python visualization libraries that carry real production weight in 2026: matplotlib, seaborn, plotly, altair, bokeh and plotnine. Code is real and runnable. There are no invented benchmark numbers here, because rendering speed depends so heavily on your data shape, browser and backend that any single figure would be misleading. What is stable and worth comparing is the API ergonomics and the rendering model.

What python data visualization libraries actually compete on

Strip away marketing and every library in this space differs on four axes.

Rendering model. Some libraries rasterize or vectorize inside Python and hand you bytes: a PNG, an SVG, a PDF. Others emit a JSON specification plus a JavaScript runtime, and the browser does the drawing. matplotlib, seaborn and plotnine are in the first camp. plotly, altair and bokeh are in the second. This single split determines whether your chart works in a LaTeX paper, an email attachment, a headless CI job, or a live web page.

API style. Imperative libraries make you build a figure by mutating axes objects. Declarative libraries make you describe a mapping from data columns to visual channels and let the library resolve the rest. matplotlib is imperative to the bone. Altair and plotnine are declarative grammars. seaborn and plotly express sit in a pragmatic middle: high-level calls that produce a full chart, with an escape hatch down to the lower layer.

Statistical machinery. Do you have to compute the aggregation yourself, or does the library do it? seaborn will bootstrap confidence intervals around a line, fit a regression, and build a KDE without you touching scipy. Altair and plotnine have transform and stat layers that run inside the chart specification. matplotlib will draw exactly what you hand it and nothing more.

Escape hatch quality. Every library looks fine until a stakeholder asks for a second y-axis with a log scale and an annotation pinned to a data coordinate. What matters then is how far you can drop below the friendly API without rewriting the chart. matplotlib has no ceiling because it is the floor. Altair's ceiling is Vega-Lite's grammar, and when you hit it you are stuck writing raw Vega.

Choosing which of the data visualization tools Python offers is easier once you accept that these four axes trade against each other. Nothing gives you a one-line API, total low-level control, and a browser-interactive output simultaneously.

matplotlib and seaborn: the imperative pair

matplotlib is the substrate. Almost every other option in this space either wraps it (seaborn, plotnine, pandas.DataFrame.plot) or defines itself against it. It renders through pluggable backends, most commonly Agg for headless PNG output, and it will produce vector PDF and SVG cleanly, which is why it remains the default for anything going into print or a journal submission.

The cost is verbosity. You are addressing an Axes object and setting properties one at a time.

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(8, 4.5))
ax.plot(df["month"], df["mrr"], color="#111111", linewidth=1.5)
ax.set_ylabel("MRR (USD)")
ax.set_xlabel("Month")
ax.spines[["top", "right"]].set_visible(False)
ax.grid(axis="y", alpha=0.25)
fig.savefig("mrr.pdf", bbox_inches="tight")

That verbosity is also the reason matplotlib never blocks you. Dual axes with ax.twinx(), annotations in data coordinates, custom colormaps, inset axes, GridSpec layouts: it is all there, documented, and stable across years of code.

seaborn is matplotlib with opinions and statistics. sns.set_theme() fixes the visual defaults that made matplotlib look dated, and the figure-level functions (relplot, catplot, displot, lmplot) handle faceting by a categorical column without you writing a subplot loop.

import seaborn as sns

sns.set_theme(style="whitegrid")
g = sns.relplot(
    data=df, x="month", y="mrr", hue="plan", col="region",
    kind="line", height=3.5, aspect=1.4, errorbar=("ci", 95),
)
g.set_axis_labels("Month", "MRR (USD)")
g.savefig("mrr_by_region.png", dpi=200)

The errorbar=("ci", 95) argument is the point. seaborn bootstrapped that interval for you. Doing the same in matplotlib means computing the resample yourself and calling fill_between.

seaborn also ships the newer objects interface, seaborn.objects, which is a genuine grammar-of-graphics API layered on the same rendering stack. It is worth learning if you like declarative composition but need matplotlib output:

import seaborn.objects as so

(
    so.Plot(df, x="month", y="mrr", color="plan")
    .add(so.Line(), so.Agg())
    .add(so.Band(), so.Est(errorbar="se"))
    .label(y="MRR (USD)")
)

Practical note that catches people: seaborn's figure-level functions return a FacetGrid, not a Figure. Calling plt.savefig() after them sometimes saves the wrong thing. Use g.savefig().

plotly and bokeh: the browser-native pair

plotly builds a JSON figure spec that plotly.js renders in a browser. Hover tooltips, zoom, pan, legend toggling and box select are free and on by default. In a notebook it renders inline; exported to HTML it is a self-contained interactive page.

import plotly.express as px

fig = px.line(df, x="month", y="mrr", color="plan", markers=True,
              labels={"mrr": "MRR (USD)", "month": "Month"})
fig.update_layout(hovermode="x unified", template="plotly_white")
fig.write_html("mrr.html", include_plotlyjs="cdn")

plotly.express is the high-level entry point; plotly.graph_objects is the layer beneath it, and dropping down is painless because express returns a Figure you keep mutating. Static export works through fig.write_image(), which needs the Kaleido engine installed. That extra dependency is the usual friction point in CI containers.

The trade-off is weight. An HTML file with include_plotlyjs="cdn" is small but requires network access to render. Inline the bundle with include_plotlyjs=True and the file becomes large but works offline. For a report emailed to an executive, that choice matters more than any styling decision you will make.

bokeh takes the browser-first idea further by shipping a server. A plain bokeh chart is static HTML plus BokehJS, same as plotly. But bokeh serve runs a Python process that keeps the chart's data source live, so a widget callback can execute real Python, query a database, and push new data into the open browser session.

from bokeh.plotting import figure, save
from bokeh.models import ColumnDataSource, HoverTool

src = ColumnDataSource(df)
p = figure(x_axis_type="datetime", height=340, sizing_mode="stretch_width",
           tools="pan,box_zoom,wheel_zoom,reset,save")
p.line("month", "mrr", source=src, line_width=2)
p.add_tools(HoverTool(tooltips=[("Month", "@month{%F}"), ("MRR", "@mrr{$0,0}")],
                      formatters={"@month": "datetime"}, mode="vline"))
p.yaxis.axis_label = "MRR (USD)"
save(p, filename="mrr.html", title="MRR")

ColumnDataSource is bokeh's central abstraction and the reason it composes well into apps: multiple glyphs share one source, so a selection in one plot highlights the same rows in another with no glue code. If you are building an internal tool where the chart is the interface rather than the output, bokeh earns its extra concepts. If you just want an interactive line chart in a notebook, it is more machinery than the job requires.

altair and plotnine: the grammar pair

altair is a Python API over Vega-Lite. You never draw anything. You declare an encoding: this column goes on x as temporal, that one on y as quantitative, this one becomes color as nominal, and Vega-Lite decides scales, axes, legends and interactions.

import altair as alt

chart = (
    alt.Chart(df)
    .mark_line(point=True)
    .encode(
        x=alt.X("month:T", title="Month"),
        y=alt.Y("mrr:Q", title="MRR (USD)", scale=alt.Scale(zero=False)),
        color=alt.Color("plan:N", legend=alt.Legend(title="Plan")),
        tooltip=["month:T", "plan:N", alt.Tooltip("mrr:Q", format="$,.0f")],
    )
    .properties(width=640, height=300)
    .interactive()
)
chart.save("mrr.html")

The type suffixes (:T, :Q, :N, :O) are altair's best idea and its steepest learning step. Declaring that month is temporal and plan is nominal removes an entire category of bugs where a numeric ID gets treated as a continuous scale. Composition is equally clean: chart_a | chart_b puts charts side by side, chart_a & chart_b stacks them, and + layers them.

Two things to know before you commit. First, altair serializes your data into the chart spec by default, so it enforces a row limit and raises MaxRowsError on large frames. You can lift it with alt.data_transformers.disable_max_rows(), but the honest fix is to aggregate before charting or enable a transformer that keeps data out of the spec. Second, when your requirement falls outside Vega-Lite's grammar, there is no gentle descent. You are writing Vega by hand or switching libraries.

plotnine is a faithful port of ggplot2 to Python, including the + operator for layering. If your team already reads R, this eliminates a translation tax that is easy to underestimate.

from plotnine import (ggplot, aes, geom_line, geom_point,
                      facet_wrap, labs, theme_minimal, scale_y_continuous)

(
    ggplot(df, aes(x="month", y="mrr", color="plan"))
    + geom_line(size=1)
    + geom_point(size=1.5, alpha=0.7)
    + facet_wrap("~region", ncol=2)
    + scale_y_continuous(labels=lambda v: [f"${x:,.0f}" for x in v])
    + labs(x="Month", y="MRR (USD)", color="Plan")
    + theme_minimal()
)

Because plotnine renders through matplotlib, you get PDF and SVG export and headless rendering for free, which makes it the one grammar-of-graphics option in this list that suits publication output. The catch is that it is a port, so ggplot2 extensions from CRAN do not exist here, and some newer ggplot2 features arrive later or not at all.

Comparing the six on the properties that bite

LibraryAPI styleRendersInteractivityStats built inBest escape hatch
matplotlibImperativePNG, SVG, PDF from PythonMinimal, backend dependentNoneUnlimited, it is the base layer
seabornHigh level over matplotlibPNG, SVG, PDF from PythonSame as matplotlibStrong: CIs, KDE, regressionDrop to the Axes object
plotlyHigh level plus graph objectsJSON plus plotly.js in browserRich, on by defaultSome: trendlines, marginalsMutate the Figure object
altairDeclarative grammarVega-Lite JSON in browserSelections and linked brushingTransform and aggregate layerRaw Vega only
bokehImperative glyph and model APIBokehJS in browser, optional serverRich, plus Python callbacksMinimalFull model layer plus custom JS
plotnineDeclarative grammar (ggplot2)PNG, SVG, PDF via matplotlibMinimalStat layers, smoothersDrop to matplotlib objects

Two rows in that table explain most real-world regret. Teams pick altair for a report pipeline and discover Vega-Lite output does not go into a PDF without a headless browser step. Teams pick matplotlib for an internal tool and spend a sprint hand-rolling hover behaviour that plotly gives away.

Picking between python data visualization libraries by output target

Start from the destination, not the syntax.

Where the chart ends upPrimary pickReasonable alternativeReason
Exploratory notebook, fast iterationseabornplotly expressOne call per chart, faceting included, sensible defaults
Paper, PDF report, printmatplotlibplotnineNative vector output, precise typography control
Internal web app or dashboard pageplotlybokehInteractive by default, embeds as HTML or JSON
Live app with Python callbacks on user inputbokeh serverplotly with DashServer keeps the data source in Python
Slide deck or image for a documentmatplotlib or seabornplotly with KaleidoDeterministic raster and vector export
Data app driven by widgetsplotly inside Streamlitbokeh inside PanelFramework native integration
Team migrating from Rplotninealtairggplot2 grammar transfers directly
Statistical charts with intervalsseabornplotnineAggregation and error estimation built in
Linked, brushable multi-chart viewsaltairbokehSelections compose declaratively
Very large scatter datamatplotlib with datashaderbokeh with datashaderServer-side rasterization avoids shipping every point

If the chart type itself is undecided, sequence matters: settle the chart form before the library. Our guide on when to use different types of graphs covers that choice, and it will save you from implementing a stacked area chart that should have been a small multiples grid.

Mistakes that cost time with python data visualization libraries

Installing four of them and standardising on none. The worst outcome is a codebase where three teams each chose a different library and every chart needs a different mental model to maintain. Pick a default for your primary output target, then allow one documented exception.

Treating the notebook as the deployment target. Charts that render inline through a Jupyter extension can vanish when the same code runs in a script, a CI job, or a web server. Test the render path you will actually ship on, early.

Ignoring the static export dependency. plotly needs Kaleido for images. altair needs a browser engine (through vl-convert or similar) for PNG and PDF. Discovering that inside a locked-down container on release day is a bad afternoon.

Aggregating in the chart when you should aggregate in the query. Every browser-rendered library serializes data into the page, so pushing a hundred thousand raw rows to a chart that displays twelve monthly points is a performance problem you created.

Rebuilding a reporting tool out of plotting code. This is the expensive one. A weekly revenue chart script becomes a cron job, becomes a small Flask app, becomes an unowned internal dashboard nobody trusts. If the actual requirement is business reporting rather than analysis, evaluate that as a tooling decision instead. Our business analytics software buyer's field guide walks the category, and the head-to-head in Domo vs Power BI shows how the incumbent platforms price and position against each other.

Charting data that lives in a SaaS tool with no warehouse behind it. Pulling numbers out of a project tracker or a wiki through an API just to plot them is often more work than the insight justifies. The reporting limits of those tools are real and specific, as the write-ups on Notion analytics and Asana analytics lay out.

Honorable mentions worth knowing about

pandas.DataFrame.plot() is a matplotlib wrapper and the fastest path to a throwaway chart during exploration. It is not a library choice, it is a shortcut. hvplot gives the same one-liner ergonomics but renders through HoloViews to bokeh or plotly, a nice compromise when you want brevity with interactivity. datashader is not a plotting library at all, it is a rasterizer: with millions of points it renders them to an image server-side and hands that image to matplotlib or bokeh, which is the correct answer to "my scatter plot froze the browser."

For anyone evaluating python libraries for data visualization inside a reporting-heavy industry, the constraint is usually data access rather than rendering. The insurance data platform guide covers what that looks like when the underlying systems are the bottleneck, and retail analytics tools does the same for store-level data.

Where Skopx fits if you landed here with a business question

Some readers arrive at an article about python visualization libraries because they are building software. Others arrive because someone asked "how did revenue trend last quarter" and charting it in Python seemed like the shortest path available.

If you are in the second group, be honest about what you are doing. Writing a script to pull from an API, reshape a dataframe, and render a chart is thirty to ninety minutes for a single answer that will be stale next week.

Skopx (see pricing) is built for that second case, and it is worth stating plainly what it is not: it is not a dashboard builder and not a BI platform. There is no canvas where you drag chart widgets onto a grid. Instead of building a dashboard, you connect the tools your company already runs, nearly 1,000 of them including Gmail, Slack, Stripe, HubSpot, QuickBooks and Google Analytics, and ask the question in chat. The answer comes back with citations pointing at the source records, so you can check it rather than trust it.

Alongside that: a morning brief that summarizes what moved overnight, an insights engine that surfaces risks and anomalies you did not think to query, and workflows you build by describing them in chat rather than writing and scheduling a script. Skopx Team includes 2.3 million AI tokens per seat monthly, and you can bring your own AI key for any major model at zero markup. Pricing is $5 per month for Solo and $16 per seat per month for Team.

A recurring metric summary is exactly the kind of thing that ends up as an unmaintained cron job and a plotting script:

Monday revenue summary without a plotting script

Monday 08:00

Recurring schedule set in chat

Pull Stripe revenue

Last week and prior week, by plan

Pull signup counts

Google Analytics conversions

Compare periods

Deltas plus anything outside normal range

Post to Slack

Short summary with links to source records

A described-in-chat workflow that replaces the weekly chart job most teams write in Python.

If you genuinely need a chart, keep using matplotlib. If you need an answer, plotting code is a detour.

Frequently asked questions

Which of the python data visualization libraries should a beginner learn first?

seaborn, with enough matplotlib underneath it to adjust labels and save figures. seaborn produces a defensible chart in one call and teaches you the vocabulary of tidy data, hue mapping and faceting. Because it renders through matplotlib, everything you learn about axes, figures and export carries forward to any matplotlib-based work later. Learn altair or plotnine second, once you want a grammar rather than a set of functions.

Is matplotlib still worth using in 2026?

Yes, and not just for legacy reasons. It is the only option in this comparison with no ceiling: any chart you can describe geometrically, you can draw. It produces true vector PDF and SVG for publication. It runs headless with zero browser dependencies. seaborn, plotnine and pandas.plot() all sit on top of it, so competence with matplotlib pays off across the ecosystem regardless of which high-level API you prefer.

plotly or bokeh for an interactive dashboard?

If the chart is the deliverable and the page is mostly static around it, plotly is less work: plotly.express gets you there faster and the Dash ecosystem is larger. If the application needs Python to run in response to user interaction on live data, bokeh's server model is the more direct architecture, since callbacks execute in Python against a shared ColumnDataSource rather than round-tripping through a separate framework.

Can altair handle large datasets?

Not by default. Altair embeds your data into the Vega-Lite specification, so it raises MaxRowsError past its row limit to stop you shipping an enormous JSON blob to the browser. You can disable the guard, point the chart at an external data URL, or use a data transformer that keeps rows out of the spec. The better habit is aggregating before you chart, which is good practice with every browser-rendered library, not just altair.

Do I need Python at all for a one-off business chart?

Often not. If the question is "what happened to revenue by plan last quarter" and the data lives in Stripe or a CRM, writing an extract-and-plot script is a slow way to get one number in a picture. Asking the question against connected tools and getting a cited answer is faster and does not create a script somebody has to maintain. Save the plotting code for analysis that genuinely needs custom visual work, model diagnostics, or anything headed for publication.

How many of these libraries should one team standardise on?

Two. One default for your main output target, and one exception for the case the default handles badly, most commonly a static library plus an interactive one. Three or more means every code review requires a context switch and shared plotting helpers stop being reusable.

Share this article

Skopx Team

The Skopx engineering and product team

Related Articles

Stay Updated

Get the latest insights on AI-powered code intelligence delivered to your inbox.