Common Mistakes

The errors that produce a chart which looks fine and is not.

Overview

The plot lands somewhere else

matplotlib has two interfaces. The pyplot interface (plt.plot) tracks a current figure and a current axes behind the scenes. The object-oriented interface (ax.plot) targets a specific axes that you name. Most mistakes come from the first one, because the hidden state changes in ways that are hard to predict.

A chart can look right on screen and be wrong in the saved file. Labels can vanish, colours can shift, and data can land on the wrong panel. None of these raise an error. The chart renders, and it renders wrong. That silence is what makes these bugs expensive: you only catch them by looking, and often only after sharing.

This page collects the mistakes that come up over and over. Each one has a symptom, a cause, and a fix. Most of the fixes are one line. The hard part is knowing which line.

The mistakes fall into a few groups. State bugs, where the current axes or figure is not the one you expect. Layout bugs, where saved output is cropped or overlapping. Scale bugs, where the axis misrepresents the data. And label bugs, where tick labels attach to the wrong positions. Each group has a pattern, and once you see the pattern you stop making the mistake.

Nearly all of these mistakes go away if you use the object-oriented interface and pass an explicit axes every time. That is the single best habit this page can teach you. The pyplot shorthand is fine for a quick look in a notebook. Anything you plan to save or share with others should name its axes.

plt.plot draws on the current axes. After plt.subplots(1, 2), the current axes is the last one created. A stray plt.plot goes to the right-hand panel even though you meant the left.

In a notebook it is worse. The current figure is what the last cell created. With out-of-order runs, that is not always the cell above.

ax.plot(...) removes the question. This is the practical reason the object-oriented API is worth the extra characters.

Worth knowing

An empty plot is empty data, all-NaN data, or limits pointing elsewhere — len(ax.lines) tells you whether the draw happened.
plt.plot lands on the current axes, which after subplots() is the last one created. Use ax.plot.
A figure saves at its declared size, not the window's, so a layout that fits on screen can be cropped in the file.
Colours of similar luminance become identical in greyscale; varying linestyle and marker makes a chart work without colour.
Setting limits that exclude data clips it silently — check max(data) against get_ylim().
Figures left open warn after twenty and keep consuming memory; plt.close(fig) in any drawing loop.

Common Mistakes

The errors that give a chart which looks fine and isn't.

Nothing appears

Three causes, and how to tell which one you have.

example_01.pymatplotlib
Output

matplotlib rarely raises an error when something goes wrong visually. An empty dataset, a mismatched array, a colour that blends into the background. All of these produce a figure without complaint. The figure just does not show what you expected.

That silence is by design. matplotlib draws what you tell it to draw. If the data is empty, it draws nothing. If the limits exclude the data, it draws an empty axes. If the colour matches the background, it draws an invisible line. The library has no opinion about whether the result looks right.

This page collects the mistakes that produce a chart which renders without error but does not show the right thing. Each section starts with the symptom, explains why it happens, and gives the fix. The mistakes are sorted from most common to least.

The page also covers mistakes of interpretation: charts that display the data correctly but mislead the reader. A truncated y axis, a rainbow colourmap, a dual-axis chart with misaligned scales. These are not bugs in your code. They are bugs in your chart's argument. Knowing the difference matters because the fix is different. A display bug is a code change. An interpretation bug is a design change.

Debugging a figure that looks wrong starts with three questions. Did the draw call run? Is the data what you think it is? Are the limits showing the right range? Answering all three takes a few seconds and locates the problem in most cases.

The final section is a pre-flight checklist: the things to verify before sharing or saving a figure. Labels, units, legend, source, title, axis range and colour accessibility. Running through the list catches most issues before they reach the audience.

Three causes, distinguishable in a few seconds.

No data. len(x) and len(y). An empty array plots without complaint.

All missing. np.isnan(y).all(). NaN is skipped. So An all-NaN series draws an empty line object.

The view is elsewhere. ax.get_xlim() and get_ylim(). Limits set before the data was added, or set to the wrong range, put the data outside the visible box.

len(ax.lines) sets apart "the draw call never ran" from "it ran and there's nothing to see". This Is often the fastest thing to check.

A fourth possibility in a script: you never showed or saved the figure. In a notebook figures display on its own. In a script they don't. plt.show() or savefig is needed.

The plot goes to the wrong axes

Because plt.* uses the current figure, whatever that now is.

example_02.pymatplotlib
Output

Overlapping labels in a saved file

It looked fine on screen because the screen was a different size.

example_03.pymatplotlib
Output

Colours that do not survive

Greyscale is the quickest test of whether the encoding works.

example_04.pymatplotlib
Output

Silent truncation and missing categories

The chart draws happily with part of the data missing.

example_05.pymatplotlib
Output

Figures left open

The warning that appears after twenty, and what it means.

example_06.pymatplotlib
Output

matplotlib keeps a link to each figure created through pyplot. They're never collected while it holds them.

After twenty, it warns: "More than 20 figures have been opened." That is a warning, not an error. The loop continues. Memory keeps growing until a thing else fails.

plt.close(fig) in the loop. plt.close("all") between sections of a notebook.

The threshold is rcParams["figure.max_open_warning"], and raising it to silence the message is the wrong response to a real problem.

Cropped output

A figure is saved at the size figsize declares, at dpi detail. The window you were looking at has nothing to do with it.

So a chart whose labels fit on screen can be cropped in the file. The first time this happens it looks like matplotlib lost the label.

tight_layout() or constrained_layout=True reserve the space. bbox_inches="tight" at save time expands the output to include all.

Rotated tick labels and long y-axis labels are the usual casualties. A chart type that doesn't need rotation. barh rather than bar. Avoids the problem fully.

Colour that does not survive

Colours chosen to be visually distinct are often not distinct in luminance. Luminance is what remains in greyscale and what colour-blind readers lean on most.

Red, green and dark red are three obviously other colours with nearly the same brightness. Printed in black and white, they're one colour.

Two habits fix it: use a palette designed for the purpose. The viridis family, or Okabe–Ito for groups. And vary a second channel so colour is reinforcing rather than carrying the meaning.

The greyscale test is quick and catches most of it.

Silent clipping

Setting limits that exclude data doesn't warn. The excluded points are simply not drawn. A bar taller than the axis runs off the top looking like any other tall bar.

That is a serious failure: the reader sees a chart where one group is four times the next largest. Has no way to know.

matplotlib can't warn about it. Clipping is often just what you want. Zooming into a region is a legitimate step.

The check is one line: compare max(data) against ax.get_ylim()[1]. Where a value is truly off-scale and the chart must stay zoomed, say so with an annotation giving the real number.

The related version is a group filtered out earlier in the pipeline and never noticed. This Is why checking the number of bars against the number of groups is worth doing.

A short list

Use ax. methods, not plt..

Check the data before blaming the chart: length, NaN, limits.

Save with layout management, and look at the saved file rather than the screen.

Test in greyscale.

Compare the data's range against the axis limits.

Close figures in loops.

And the one from the design module that outranks all of them. Know what the chart is for before drawing it. None of these checks help a chart that is answering the wrong question.

Mistakes of interpretation

Beyond the rote errors, a set of chart-level mistakes give output that is strictly correct and misleading.

A truncated bar axis. Length must be read from zero.

Dual axes. The crossing point is a choice, not a finding.

Unshared axes across panels. The layout invites a compare the scales don't support.

An uncentred diverging colormap. Half the data is coloured as though it had the opposite sign.

A line through sparse points. It asserts values between the findings.

A pie with many slices. The reader can't rank them.

A mean without a spread. Two very other spreads look same.

Each of these is a default that matplotlib will happily give. This Is why they're worth knowing as a list.

Mistakes of omission

Things whose lack is the error:

No units on the axis labels.

No indication of sample size, when it varies between groups.

No note that an axis is logarithmic. This Changes how each distance on it should be read.

No statement of what an error bar shows.

No caption saying when the data is from and what it covers.

These cost a line each and are the gap between a chart that can be acted on and one that has to be asked about.

Debugging a figure

A routine that resolves most problems quickly:

Print the data. Shape, range, count of NaN. Most "the chart is wrong" is "the data isn't what I thought".

Check the artists. len(ax.lines), len(ax.patches), len(ax.sets). Did the draw call happen?

Check the view. ax.get_xlim(), get_ylim() against the data's range.

Save it and look at the file. This Is what other people will see.

Look at it in greyscale. This Catches encoding problems that colour hides.

In that order, because each is cheaper than the next.

A pre-flight checklist

Before a chart is shared:

Does the title state the finding?

Do the axes have units?

Is the baseline right?

Are the panels close, if the layout implies they should be?

Is anything clipped, hidden or overplotted?

Does it work without colour?

Is there one message?

Is the source and date on it?

Eight questions, most answered in seconds. Between them they catch nearly all in this module.

The general lesson

matplotlib will draw what it's asked. It has no view about whether the chart is honest, readable or answering a question anyone asked.

That is the right design for a library and it means the job sits with the person writing the call. The rote parts of this track. The args, the objects, the defaults. Are the easy half. The choices are the half that finds whether the chart was worth drawing.

Mistakes that look like matplotlib bugs

A few actions get reported as bugs and are working as designed.

A figure appears blank in a script. Nothing called show or savefig.

Colours skipped — passing c= where color= was meant, or a colour list to a function taking a single colour.

**set_facecolor doing nothing on a box plot** — patch_artist=True wasn't passed.

Ticks reappearing after being set. A later autoscale replaced the locator.

A legend showing one entry for many lines. One label on a call that drew a few.

**Text invisible after transparent=True** — the foreground was never changed.

Each has a one-line fix and none of them raises. This Is what makes them frustrating rather than hard.

Building the habit

The checks in this module are worth turning into a routine rather than remembering on their own.

A short function that draws and then asserts is one way:

def finish(ax, title, xlabel, ylabel):
    ax.set_title(title, loc="left", fontweight="bold")
    ax.set_xlabel(xlabel); ax.set_ylabel(ylabel)
    for s in ("top", "right"):
        ax.spines[s].set_visible(False)
    assert ax.get_title(), "no title"
    return ax

It enforces the labels, applies the house treatment, and fails if a thing was skipped.

More often, the mistakes in this track fall into two groups. Things matplotlib won't tell you (clipping, wrong axes, cropped output) and things no one will tell you (a misleading baseline, an uninterpretable interval, a chart answering the wrong question). The first group is caught by checking. The second only by asking what the chart is for.

The two kinds of error

It's worth separating them, because they're found in other ways.

Errors matplotlib could tell you about but doesn't. A plot on the wrong axes, a clipped bar, a cropped label, an empty series, a figure never closed. These are rote, they've definite answers. The checks in this module find them in seconds. They're also the ones that give a chart which is obviously odd once looked at.

Errors no one can tell you about. A truncated baseline, an uncentred diverging scale, a dual axis, a mean without a spread, a chart answering a question no one asked. These give output that is strictly correct and misleading. No check catches them because nothing is wrong with the code.

The first kind is fixed by looking at the chart. The second is fixed by asking what the chart claims and whether the data supports it. Which is a other activity. The one that matters more.

Common Mistakes, in summary

An empty plot is empty data, all-NaN data, or limits pointing elsewhere. len(ax.lines) says whether the draw happened.

plt.plot lands on the current axes. This Is rarely the one you meant.

A figure saves at its declared size. Look at the file rather than the screen.

Limits that exclude data clip it silently. A too-tall bar looks like any other tall bar.

Colours of similar luminance vanish in greyscale.

Figures left open warn after twenty and keep consuming memory.

And the checklist that catches most of it. Title states the finding, units on the labels, correct baseline, nothing hidden, works without colour, one message, source and date. Takes under a minute and is the gap between a chart that is finished and one that is merely drawn.

A worked debug

A concrete example of the routine, on the most common complaint: "the chart is wrong".

Look at the data going in. df.shape, df.head(), df.dtypes, df.isna().sum(). About half of all wrong charts are correct renderings of wrong data. This finds them before any plotting is checked.

Check the artists. Did the draw call happen. How many things did it create? A legend with one entry where five were expected means one call drew five lines with one label.

Check the view. Limits against the data's range.

Check the axes. Is the plot on the axes you think? len(ax.lines) on each.

Look at the saved file, not the screen.

Look at it in greyscale.

The order matters because each step is cheaper than the next. The first one resolves the majority. The instinct to start by reading matplotlib docs is often the slowest on hand route.

Questions people ask about Common Mistakes

What is the most common mistake? A mismatch between what the chart says and what the reader takes from it, such as a truncated bar axis or an unlabelled unit.

Why do my plots stack up on the same figure? Without a new figure or ax.clear(), plt keeps drawing to the current axes. Create a figure per chart.

Why does my chart show nothing in a script? You forgot plt.show(), or you saved before drawing. Check the order of calls.

Is it a mistake to use default colours? Not by itself. It becomes one when colours carry no meaning, or when they are hard to tell apart.

Why do I get a warning about the legend? No artist had a label. Pass label= when you plot.

How do I avoid the most common errors? Label axes with units, start bars at zero, limit series per chart, and look at the figure at the size it will be viewed.

Common Mistakes: the short version

Two kinds of error: the ones matplotlib could report and doesn't. The ones no one can report because nothing is wrong with the code.

The first are found by looking. At the data, the artists, the limits, the saved file. The second are found by asking what the chart claims and whether the data supports it. This Is the harder and more valuable habit.

Check yourself

0 of 4

Answer without scrolling back up.

  1. Your plot is empty. What distinguishes 'the draw never ran' from 'there is nothing to see'?

  2. After `plt.subplots(1, 2)`, where does a stray `plt.plot` land?

  3. Why can a chart look fine on screen and be cropped when saved?

  4. What happens when axis limits exclude some of the data?

Cheat sheet

Common Mistakes

Spot the matplotlib mistakes that produce a chart which looks correct but misrepresents the data. Fix truncated axes and dual-axis traps.

MATPLOTLIB · vizlearn.in/matplotlib/common_mistakes.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.