Intermediate 5 min · March 05, 2026

Matplotlib — Blank PNGs from plt.show() Before savefig()

All saved PNGs are blank despite no errors — plt.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
Quick Answer
  • 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() before plt.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 plt.plot() and it works — until it doesn't. The reason it eventually breaks is they never understood the two-layer architecture underneath every Matplotlib chart.

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 plt.plot() 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.

The professional habit is to always explicitly create your Figure and Axes with plt.subplots(). 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.

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 plt.style.use(). The most useful ones for professional contexts are 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 tight_layout() or the newer 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 savefig() before show(). When show() runs, it renders the figure to the screen and then destroys it. Any savefig() call after show() saves an empty figure. This is the most common Matplotlib bug in automated scripts.

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 plt.subplots() or plt.figure() call creates an OS-level window handle that persists in memory until you call plt.close(). In scripts that generate thousands of charts (e.g., batch reporting), this leaks memory and can crash the script with 'Too many open figures'.

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 plt.subplots() function with nrows and ncols creates a grid of Axes. But the defaults can create cramped, overlapping layouts that make your figure unreadable.

1. constrained_layout=True: This is the easiest way to automatically adjust spacing between subplots. It replaces the older tight_layout() with a more intelligent algorithm that respects colorbars, legends, and axis labels. Enable it at figure creation: 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.

Aspectplt.plot() Implicit Stylefig, ax = plt.subplots() Explicit Style
Code readabilityShorter for quick experimentsLonger but self-documenting
Multiple subplotsError-prone — global state bleedsClean — each ax is isolated
Reusable in functionsFragile — hidden global stateSafe — pass ax as argument
Saving files correctlyOften works accidentallyPredictable, always correct
Best forREPL / Jupyter explorationScripts, apps, dashboards
Customization depthLimited access to figure propertiesFull control over Figure and Axes
Team code reviewHard to follow intentObvious what each line affects

Key Takeaways

  • Always use fig, ax = plt.subplots() — never rely on Matplotlib's implicit global state once your code goes beyond a single quick plot.
  • Plot type choice is a data communication decision: line for time-series continuity, bar for category comparison, scatter for relationships, histogram for distributions.
  • Call plt.savefig() before plt.show() — this order is mandatory or your file will be blank.
  • 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 call plt.savefig('name.png') first, then plt.show(). This order is non-negotiable because show() clears the figure from memory.
  • 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: Use ax.set_title() on the specific Axes object you're working with. plt.title() always operates on the current axes, which changes unpredictably when you have multiple subplots.
  • 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: Add plt.close(fig) at the end of each loop iteration. Alternatively, call plt.close('all') after batch operations. Use len(plt.get_fignums()) to monitor open figure count.
  • 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 pass bbox_inches='tight' to plt.savefig(). This tells Matplotlib to expand the saved bounding box to include all artists, including out-of-bounds legends.

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
    A Figure is the entire canvas that contains everything: title, padding, and all subplots. An Axes is the actual plotting area with its own coordinate system, x/y-axis, and data. When you use the implicit API (plt.plot()), Matplotlib creates both silently and attaches the plot to whichever axes is 'current'. In production code, this leads to state bugs where plots end up on wrong subplots. The explicit fig, ax = plt.subplots() pattern gives you direct control — you know exactly which axes each plot call targets. This is critical when your script generates multiple charts programmatically or when you pass axes objects into functions.
  • QWhy would a chart appear correctly in a Jupyter notebook but the saved PNG file be blank?JuniorReveal
    The most likely cause is calling plt.savefig() after plt.show(). In a notebook, inline rendering shows the chart correctly, but plt.show() in a standalone script renders and then destroys the figure. Any savefig() call after that saves an empty figure. The fix: always call savefig() first. A secondary cause: the notebook might be using an interactive backend that shows the chart even when the underlying figure object is empty. Always test your save logic by running the script from the command line, not just in a notebook.
  • QWhen would you choose a histogram over a bar chart, and what happens if you use the wrong one?Mid-levelReveal
    Histograms display the distribution of a single continuous variable by binning the data (bars touch). Bar charts compare discrete categories (bars have gaps). If you use a bar chart for continuous data, you lose information about the distribution density because the gaps falsely separate the data into categories. If you use a histogram for categorical data, the adjacent bins imply a continuity that doesn't exist, misleading your audience into thinking categories are ordered on a continuous scale. The rule: histogram for distributions, bar chart for comparisons.
  • QHow would you debug a memory leak caused by generating many Matplotlib figures in a loop?SeniorReveal
    First, confirm the leak by adding print(len(plt.get_fignums())) inside the loop — if the count keeps increasing, figures aren't being closed. The root cause is that each plt.subplots() or plt.figure() creates a new figure object that persists until explicitly closed. The fix is to call plt.close(fig) at the end of each loop iteration. For batch scripts, you can also use a context manager to ensure closure: with plt.ion() but better to use plt.ioff() with manual close. On servers with limited memory (e.g., AWS Lambda), you may also need to set plt.switch_backend('Agg')` to prevent GUI figure creation at all.

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

Previous
Pandas DataFrames
5 / 51 · Python Libraries
Next
Seaborn for Data Visualisation