2025, Oct 02 07:00

Fix crowded Seaborn legends in Matplotlib 3.10+: filter handles and labels to hide unwanted entries

Clean up Seaborn legends in Matplotlib 3.10+: filter handles and labels to hide unwanted entries without underscores. Step-by-step fix with code example.

When a Seaborn plot includes multiple lines and error bands, the legend can get crowded. A common trick used to be passing underscores into matplotlib’s legend call to hide unwanted entries. In recent matplotlib versions (>3.10), that behavior was removed, which breaks the old pattern and leaves you with labels you no longer want to show.

Minimal example of the problem

The following snippet creates a multi-line Seaborn plot and then tries to hide some legend entries with underscores. This used to suppress labels, but not anymore in the latest matplotlib.

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

data_tbl = pd.DataFrame({'timepoint': np.random.randint(0, 10, 1000),
      'class': np.random.choice(['cat', 'dog', 'duck', 'hare'], 1000),
      'probability': np.random.rand(1000)
})
sns.lineplot(data_tbl, x='timepoint', y='probability', hue='class')

# previously, providing underscores as names was hiding the label
plt.legend(['cat', *['_']*5, 'hare'])

What’s going on

The underscore hack relied on legacy behavior that no longer exists. With Seaborn, you also don’t directly control the underlying matplotlib plot calls, so you can’t set labels on the fly. The key is to work with what Seaborn already created: the legend handles and labels attached to the axes. If you select only the items you care about and pass them back to the legend, you get a clean, selective legend without relying on deprecated behavior.

The fix: filter legend items produced by Seaborn

Store the Seaborn lineplot in a variable, extract the generated handles and labels, select the ones you want to keep, and pass that filtered set back to legend. This keeps your code explicit and compatible with current matplotlib.

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

data_tbl = pd.DataFrame({'timepoint': np.random.randint(0, 10, 1000),
      'class': np.random.choice(['cat', 'dog', 'duck', 'hare'], 1000),
      'probability': np.random.rand(1000)
})
axis_obj = sns.lineplot(data_tbl, x='timepoint', y='probability', hue='class')

# grab all handles/labels created by seaborn
all_handles, all_labels = axis_obj.get_legend_handles_labels()

# decide which ones to keep
show_only = ["cat", "hare"]
filtered_handles = [hh for hh, ll in zip(all_handles, all_labels) if ll in show_only]
filtered_labels  = [ll for ll in all_labels if ll in show_only]

axis_obj.legend(filtered_handles, filtered_labels)
plt.show()

Why this matters

Legend clarity is part of plot readability, especially when conveying results with multiple categories and uncertainty bands. Relying on removed behavior is fragile; explicitly filtering legend entries makes your intent obvious and your plots consistent across versions. It also plays well with Seaborn’s higher-level API, where the plotting calls aren’t directly under your control.

Takeaways

Skip the old underscore hack. Treat the legend as a post-processing step: capture the axes from Seaborn, extract handles and labels, and feed back only the entries you want users to see. This small change keeps your visualization pipeline clean, predictable, and future-proof.

The article is based on a question from StackOverflow by skjerns and an answer by chitown88.