Matplotlib — Blank PNGs from plt.show() Before savefig()
All saved PNGs are blank despite no errors — plt.
- Matplotlib builds charts on a two-layer architecture: Figure (canvas) and Axes (plot area)
- Use explicit
fig, ax = plt.subplots()for any code that leaves a Jupyter notebook - Line for time series, bar for categories, scatter for relationships, histogram for distributions
- Saving a PNG requires
plt.savefig()beforeplt.show()— reverse order produces blank files - Production memory leaks happen when figures aren't closed in loops — always call
plt.close(fig)
Every spreadsheet has a 'Insert Chart' button for a reason — humans don't think in rows of numbers, we think in shapes, trends, and colors. Data scientists, analysts, and backend engineers who can't visualize their data are flying blind. Matplotlib is the foundational charting library in Python that powers everything from academic research papers to financial dashboards at hedge funds. If you're working with data in Python, this isn't optional knowledge — it's table stakes.
Before Matplotlib, visualizing Python data meant exporting CSVs, opening Excel, clicking through menus, and praying the chart updated when the data changed. Matplotlib solves that by letting you generate publication-quality charts programmatically — meaning your charts are reproducible, automatable, and version-controllable. It integrates tightly with NumPy and Pandas, the two libraries you're almost certainly already using.
By the end of this article you'll understand Matplotlib's Figure/Axes architecture (the part everyone skips and then regrets), know which plot type to reach for in real scenarios, be able to customize charts so they don't look like defaults, and avoid the three mistakes that trip up even experienced developers when they pick up this library.
The Figure and Axes Architecture — Why It Matters Before You Plot Anything
Most beginners jump straight to and it works — until it doesn't. The reason it eventually breaks is they never understood the two-layer architecture underneath every Matplotlib chart.plt.plot()
A Figure is the entire canvas — the window or image file that holds everything. An Axes object is the actual plot area inside that canvas, complete with its own x-axis, y-axis, title, and data. One Figure can hold multiple Axes objects, which is how you build subplots.
When you call without setting up a Figure first, Matplotlib silently creates both for you. That's convenient for quick exploration, but in any production or multi-panel context it causes chart bleeding, wrong titles showing up on wrong plots, and state bugs that are genuinely confusing to debug.plt.plot()
The professional habit is to always explicitly create your Figure and Axes with . It returns both objects, you control them directly, and your code becomes predictable. Think of it as the difference between renting a kitchen (implicit) vs owning one (explicit) — you always know where the knives are.plt.subplots()
Choosing the Right Plot Type — Line, Bar, Scatter, and Histogram Explained
Picking the wrong chart type is like using a ruler to measure temperature — technically you're measuring something, but not what you think. Each plot type answers a specific question about your data, and understanding that mapping is what separates charts that communicate from charts that confuse.
Line charts answer 'how does this change over time?' They imply continuity — every point is connected to the next. Use them for time-series data like stock prices, server latency, or user growth.
Bar charts answer 'how do discrete categories compare?' There's no implied connection between bars. Use them for comparing products, regions, or experiment groups.
Scatter plots answer 'is there a relationship between two continuous variables?' Use them to spot correlations — like ad spend vs conversions, or study hours vs exam scores.
Histograms answer 'how is this single variable distributed?' They're the go-to for understanding spread, skew, and outliers in a dataset — salary distributions, response times, and test scores all live here.
The code below demonstrates all four on meaningful data so you can see the contrast in one shot.
Styling Charts So They Don't Look Like 1995 — Themes, Colors, and Layout
Default Matplotlib charts work, but they're immediately recognizable as defaults — and that's a problem when you're presenting to stakeholders or publishing results. The good news is that production-quality styling requires fewer than 10 extra lines.
Matplotlib ships with built-in stylesheets you can activate with . The most useful ones for professional contexts are plt.style.use()seaborn-v0_8-whitegrid (clean, modern, great for business dashboards), fivethirtyeight (bold, editorial), and ggplot (familiar to R users).
Beyond stylesheets, the two highest-impact customizations are color palettes and typography. Custom hex colors make your charts match brand guidelines. Increasing font sizes to at least 12pt means your chart is readable when embedded in a presentation or PDF — the default sizes are designed for interactive notebook views, not slides.
Layout management with or the newer tight_layout()constrained_layout=True parameter prevents the single most common aesthetic bug: labels overlapping or being clipped. Enable it by default on every chart and you'll never chase that issue again.
Saving and Exporting Charts — The 3 Rules That Prevent Blank Files
Saving a chart seems trivial — call savefig() and you're done. But three gotchas cause the vast majority of production file-export bugs: wrong order with show(), missing bbox_inches='tight', and forgetting to close figures in loops.
Rule 1: Always call before savefig(). When show() runs, it renders the figure to the screen and then destroys it. Any show() call after savefig() saves an empty figure. This is the most common Matplotlib bug in automated scripts.show()
Rule 2: Always use bbox_inches='tight' when legends or annotations are placed outside the axes. Without this, Matplotlib clips the saved image to the exact figure boundary. Your legend, title, or labels get cut off even though they appear fine in the interactive window.
Rule 3: Always close figures you create in loops. Each or plt.subplots() call creates an OS-level window handle that persists in memory until you call plt.figure(). In scripts that generate thousands of charts (e.g., batch reporting), this leaks memory and can crash the script with 'Too many open figures'.plt.close()
Managing Multiple Subplots — Layouts, Sharing Axes, and Avoiding Overlap
Once you need to present multiple related charts together, you hit Matplotlib's subplot system. The function with plt.subplots()nrows and ncols creates a grid of Axes. But the defaults can create cramped, overlapping layouts that make your figure unreadable.
Three techniques fix this:
1. constrained_layout=True: This is the easiest way to automatically adjust spacing between subplots. It replaces the older with a more intelligent algorithm that respects colorbars, legends, and axis labels. Enable it at figure creation: tight_layout()plt.subplots(figsize=(10,6), constrained_layout=True).
2. sharex and sharey: When comparing time series across rows, setting sharex=True aligns the x-axes so they scroll and zoom together. This is essential for dashboards where you want to compare trends across subplots without independent axis ranges.
3. Manual subplots_adjust: For highly customized layouts, you can manually set wspace and hspace as fractions of the figure width and height. This gives pixel-perfect control when automated layout tools don't produce the exact result.
| Aspect | plt.plot() Implicit Style | fig, ax = plt.subplots() Explicit Style |
|---|---|---|
| Code readability | Shorter for quick experiments | Longer but self-documenting |
| Multiple subplots | Error-prone — global state bleeds | Clean — each ax is isolated |
| Reusable in functions | Fragile — hidden global state | Safe — pass ax as argument |
| Saving files correctly | Often works accidentally | Predictable, always correct |
| Best for | REPL / Jupyter exploration | Scripts, apps, dashboards |
| Customization depth | Limited access to figure properties | Full control over Figure and Axes |
| Team code review | Hard to follow intent | Obvious what each line affects |
Key Takeaways
- Always use
fig, ax =— never rely on Matplotlib's implicit global state once your code goes beyond a single quick plot.plt.subplots() - Plot type choice is a data communication decision: line for time-series continuity, bar for category comparison, scatter for relationships, histogram for distributions.
- Call
beforeplt.savefig()— this order is mandatory or your file will be blank.plt.show() - Remove top/right spines, increase font sizes to 12pt+, and use
constrained_layout=True— these three habits transform default charts into presentation-ready visuals. - Close every figure you create (
plt.close(fig)) in loops to prevent memory leaks in batch scripts.
Common Mistakes to Avoid
- Calling plt.show() before plt.savefig()
Symptom: The saved PNG is completely blank. The chart appears correctly in the notebook or interactive window but the file on disk is empty.
Fix: Always callplt.savefig('name.png')first, then. This order is non-negotiable becauseplt.show()clears the figure from memory.show() - Using plt.title() when working with subplots
Symptom: The title appears on the wrong subplot, overwrites another subplot's title, or does nothing visible.
Fix: Useon the specific Axes object you're working with.ax.set_title()always operates on the current axes, which changes unpredictably when you have multiple subplots.plt.title() - Not calling plt.close() in loops that generate many charts
Symptom: Memory usage climbs until the script crashes or slows to a crawl. You may see a RuntimeWarning about too many open figures.
Fix: Addplt.close(fig)at the end of each loop iteration. Alternatively, callplt.close('all')after batch operations. Uselen(to monitor open figure count.plt.get_fignums()) - Forgetting bbox_inches='tight' when placing legend outside axes
Symptom: The saved PNG/PDF has the legend cut off or missing, even though it appears correctly in the interactive view.
Fix: Always passbbox_inches='tight'to. This tells Matplotlib to expand the saved bounding box to include all artists, including out-of-bounds legends.plt.savefig()
Interview Questions on This Topic
- QWhat is the difference between a Figure and an Axes object in Matplotlib, and why does that distinction matter in production code?Mid-levelReveal
- QWhy would a chart appear correctly in a Jupyter notebook but the saved PNG file be blank?JuniorReveal
- QWhen would you choose a histogram over a bar chart, and what happens if you use the wrong one?Mid-levelReveal
- QHow would you debug a memory leak caused by generating many Matplotlib figures in a loop?SeniorReveal
Frequently Asked Questions
What is the difference between plt.show() and plt.savefig() in Matplotlib?
plt.show() renders the figure to your screen and then clears it from memory. plt.savefig() writes the current figure to disk as an image file. You must call savefig() first — if you call show() first, the figure is cleared and savefig() will produce a blank file.
Do I need to install Matplotlib separately or does it come with Python?
Matplotlib is not part of the Python standard library — you need to install it separately with pip install matplotlib. If you're using Anaconda or a data science environment like Jupyter through conda, it's typically pre-installed. You can verify by running import matplotlib; print(matplotlib.__version__) in a Python shell.
Why does my Matplotlib chart show up blank or nothing happens when I call plt.plot()?
In a plain Python script (not Jupyter), plt.plot() draws to a buffer but doesn't display anything until you call plt.show(). If you're in a non-interactive environment like a server or CI pipeline, there's no display at all — use plt.savefig() instead. Also make sure you haven't accidentally called plt.close() before plt.show(), which clears the buffer prematurely.
How do I set the size of my Matplotlib chart?
Set the figsize parameter when creating the figure: fig, ax = plt.subplots(figsize=(8, 6)) where the tuple is width and height in inches. You can also change an existing figure's size with fig.set_size_inches(8, 6).
What does bbox_inches='tight' do in savefig()?
It tells Matplotlib to automatically expand the saved image's bounding box to include all artists (labels, legends, annotations) even if they are placed outside the figure boundaries. Without it, any element moved outside the default plotting area will be clipped in the saved file.
That's Python Libraries. Mark it forged?
5 min read · try the examples if you haven't