Matplotlib Legend Outside the Plot: fig.legend and bbox_to_anchor

Putting a Matplotlib legend outside the plot takes two things: a figure made with layout="constrained", and fig.legend(loc="outside right upper"). The layout engine then shrinks the plot to make room, so nothing gets cut off:

fig, ax = plt.subplots(layout="constrained")
ax.plot(x, y, label="Sales")
fig.legend(loc="outside right upper")

Most tutorials still teach the older bbox_to_anchor trick, which works but tends to crop the legend. I’ll show both and measure what actually happens. The charts are real windows from Python 3.14.7 and Matplotlib 3.11.2.

Matplotlib chart with the legend placed outside the plot on the right using constrained layout
layout="constrained" plus loc="outside right upper": the plot shrinks and the legend fits beside it.

Put a Matplotlib legend outside the plot with bbox_to_anchor

This is the version you’ll find in most Stack Overflow answers. bbox_to_anchor moves the legend’s anchor point, and loc says which corner of the legend sits on it:

import matplotlib.pyplot as plt
import numpy as np

months = np.arange(1, 13)
regions = ["Northeast", "Midwest", "South", "West", "Pacific", "Mountain"]
fig, ax = plt.subplots(figsize=(8, 4.5))
for i, region in enumerate(regions):
    ax.plot(months, 40 + 6 * i + 8 * np.sin(months / 2 + i), marker="o", label=region)
ax.set_xlabel("Month (2026)")
ax.set_ylabel("Sales (thousand USD)")

legend = ax.legend(loc="upper left", bbox_to_anchor=(1.02, 1))

fig.canvas.draw()
box = legend.get_window_extent()
print(f"legend ends at {box.x1:.0f}px, the figure is {fig.bbox.width:.0f}px wide")
plt.show()

Output:

legend ends at 858px, the figure is 800px wide
Matplotlib chart where the legend placed outside the plot with bbox_to_anchor is cut off at the right edge
With no layout engine, the legend runs off the edge of the figure.

bbox_to_anchor=(1.02, 1) means just past the right edge of the axes, at the top. loc="upper left" hangs the legend’s top-left corner on that point, so the legend extends to the right.

The problem is visible in the window and in the numbers. The legend ends past the right edge of an 800 pixel figure, so the region names are chopped off.

Nothing made room for the legend. The axes still fill the figure, and the legend was pushed out beyond it.

How bbox_to_anchor values map to legend positions

The anchor is in axes coordinates, where (0, 0) is the bottom-left corner of the plot and (1, 1) is the top-right. Anything above 1 or below 0 is outside.

I checked five common pairs by measuring where the legend landed relative to the plot area:

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

def where(legend, ax):
    fig = ax.figure
    fig.canvas.draw()
    lg, ab = legend.get_window_extent(), ax.get_window_extent()
    sides = []
    if lg.x0 >= ab.x1: sides.append("right of")
    if lg.x1 <= ab.x0: sides.append("left of")
    if lg.y1 <= ab.y0: sides.append("below")
    if lg.y0 >= ab.y1: sides.append("above")
    return " and ".join(sides) or "on top of"

for loc, anchor in (("upper left", (1.02, 1)), ("center left", (1.02, 0.5)), ("upper center", (0.5, -0.12)),
                    ("lower center", (0.5, 1.02)), ("upper right", (-0.12, 1))):
    fig, ax = plt.subplots(layout="constrained")
    ax.plot([1, 2, 3], label="Sales")
    legend = ax.legend(loc=loc, bbox_to_anchor=anchor)
    print(f"loc={loc!r:<15} bbox_to_anchor={anchor!s:<12} -> {where(legend, ax)} the plot")
    plt.close(fig)
print()

for loc in ("outside left upper", "outside upper left"):
    fig, ax = plt.subplots(layout="constrained")
    ax.plot([1, 2, 3], label="Sales")
    print(f"fig.legend(loc={loc!r:<22}) -> {where(fig.legend(loc=loc), ax)} the plot")
    plt.close(fig)

Output:

loc='upper left'    bbox_to_anchor=(1.02, 1)    -> right of the plot
loc='center left'   bbox_to_anchor=(1.02, 0.5)  -> right of the plot
loc='upper center'  bbox_to_anchor=(0.5, -0.12) -> below the plot
loc='lower center'  bbox_to_anchor=(0.5, 1.02)  -> above the plot
loc='upper right'   bbox_to_anchor=(-0.12, 1)   -> left of the plot

fig.legend(loc='outside left upper'  ) -> left of the plot
fig.legend(loc='outside upper left'  ) -> above the plot

The pattern is that loc names the corner of the legend that sits on the anchor point. To go right, anchor just past 1 and hang the legend’s left side there.

Where you want itlocbbox_to_anchor
Right side, top"upper left"(1.02, 1)
Right side, middle"center left"(1.02, 0.5)
Below the plot"upper center"(0.5, -0.12)
Above the plot"lower center"(0.5, 1.02)
Left side, top"upper right"(-0.12, 1)

The second part of the output matters for the modern method below. With outside, word order changes the result: "outside left upper" goes to the left of the plot, while "outside upper left" goes above it.

Why the legend gets cut off, and how to fix it

There are several ways to make room. I measured each one on the same chart, and checked what savefig() does too:

import io
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image

months = np.arange(1, 13)

def chart(layout=None):
    fig, ax = plt.subplots(figsize=(8, 4.5), layout=layout)
    for i in range(6):
        ax.plot(months, 40 + 6 * i + 8 * np.sin(months / 2 + i), label=f"Region {i + 1}")
    legend = ax.legend(loc="upper left", bbox_to_anchor=(1.02, 1))
    return fig, legend

def edge(fig, legend):
    fig.canvas.draw()
    return f"legend ends at {legend.get_window_extent().x1:.0f}px of {fig.bbox.width:.0f}px"

fig, legend = chart()
print("no layout:          ", edge(fig, legend))
fig, legend = chart(); fig.tight_layout()
print("tight_layout():     ", edge(fig, legend))
fig, legend = chart(); fig.subplots_adjust(right=0.78)
print("subplots_adjust():  ", edge(fig, legend))
fig, legend = chart("constrained")
print('layout="constrained":', edge(fig, legend))
print()

fig, legend = chart()
for label, options in (("savefig()", {}), ('savefig(bbox_inches="tight")', {"bbox_inches": "tight"})):
    buffer = io.BytesIO()
    fig.savefig(buffer, format="png", **options)
    print(f"{label:<30} image size {Image.open(buffer).size}")

Output:

no layout:           legend ends at 852px of 800px
tight_layout():      legend ends at 785px of 800px
subplots_adjust():   legend ends at 754px of 800px
layout="constrained": legend ends at 796px of 800px

savefig()                      image size (800, 450)
savefig(bbox_inches="tight")   image size (799, 393)
Command Prompt measuring whether a Matplotlib legend outside the plot fits with tight_layout subplots_adjust and constrained layout
Every fix keeps the legend inside the figure. bbox_inches="tight" changes the saved image size instead.

fig.tight_layout() now makes room for a legend outside the axes. That’s worth knowing, because a lot of older advice says it ignores outside legends.

fig.subplots_adjust(right=0.78) works too, but you pick the number by hand. Change the labels and you’ll be tuning it again.

layout="constrained" is the one I use. It recalculates every time the figure is drawn, so longer labels or a resized window still fit.

Save the figure without cutting off the legend

savefig() kept the 800 by 450 size and cut the legend off with it. bbox_inches="tight" kept the legend by recalculating the image size, which came out at 799 by 393 here.

That’s fine for a one-off image. If you need an exact size for a report or a slide, fix the layout instead and save normally, so you get the size you asked for.

There’s more on saving charts in saving a Matplotlib chart as PNG.

The modern way: fig.legend(loc=”outside right upper”)

Matplotlib 3.7 added an outside option for figure legends. It’s what the chart at the top of this article uses:

import matplotlib.pyplot as plt
import numpy as np

months = np.arange(1, 13)
regions = ["Northeast", "Midwest", "South", "West", "Pacific", "Mountain"]
fig, ax = plt.subplots(figsize=(8, 4.5), layout="constrained")
for i, region in enumerate(regions):
    ax.plot(months, 40 + 6 * i + 8 * np.sin(months / 2 + i), marker="o", label=region)
ax.set_xlabel("Month (2026)")
ax.set_ylabel("Sales (thousand USD)")

legend = fig.legend(loc="outside right upper", title="Region")

fig.canvas.draw()
print("legend starts at", round(legend.get_window_extent().x0), "px")
print("plot area ends at", round(ax.get_window_extent().x1), "px")
plt.show()

Output:

legend starts at 674 px
plot area ends at 668 px

The legend starts to the right of where the plot area ends, so they can’t overlap. Constrained layout shrank the axes to make space, without any numbers from me.

"outside right upper" means outside the right edge, aligned to the top. The title= argument adds a heading over the entries.

There’s a catch I only found by measuring. Without layout="constrained", the same call raises no error but draws the legend on top of the plot. Always use them together.

Put the legend below the plot as a horizontal legend

Below the chart is often better than beside it, especially on a phone. Spread the entries into columns with ncols:

import matplotlib.pyplot as plt
import numpy as np

months = np.arange(1, 13)
regions = ["Northeast", "Midwest", "South", "West", "Pacific", "Mountain"]
fig, ax = plt.subplots(figsize=(8, 4.5), layout="constrained")
for i, region in enumerate(regions):
    ax.plot(months, 40 + 6 * i + 8 * np.sin(months / 2 + i), marker="o", label=region)
ax.set_xlabel("Month (2026)")
ax.set_ylabel("Sales (thousand USD)")

fig.legend(loc="outside lower center", ncols=3)
plt.show()
print("3 columns x 2 rows of legend entries under the chart")

Output:

3 columns x 2 rows of legend entries under the chart
Matplotlib chart with a horizontal legend in three columns placed below the plot
loc="outside lower center" with ncols=3 gives a two-row legend under the chart.

ncols=3 lays six entries out as two rows of three, which keeps the legend short. Older code uses ncol, which Matplotlib still accepts.

The bbox_to_anchor version works too: ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.12), ncols=3). Pair it with constrained layout so the space is reserved.

For a legend above the plot, use loc="outside upper center". It’s accepted just like the others, as the next section’s checks show.

One legend outside the plot for all subplots

With several subplots, one shared legend beats a legend on every panel. Take the handles from one axes and give them to the figure:

import matplotlib.pyplot as plt
import numpy as np

months = np.arange(1, 13)
regions = ["Northeast", "Midwest", "South", "West", "Pacific", "Mountain"]
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4.5), layout="constrained", sharey=True)
for ax, year in ((ax1, 2025), (ax2, 2026)):