To add a secondary y-axis in Matplotlib, call ax2 = ax.twinx(): it creates a second axes that shares the x-axis and puts its y-axis on the right, so two series with different scales can share one plot. If the right axis should show the same data in another unit (°C and °F, km and miles), use ax.secondary_yaxis() instead. This guide covers two y-axes, a combined legend, bar-and-line charts, three y-axes, pandas and seaborn, and secondary axes in subplots.
Tested with Python 3.12.5, Matplotlib 3.11.2, pandas 3.0.6 and seaborn 0.13.2; the plots are real Matplotlib windows. Reference: Axes.twinx and Secondary axis example.
Two y-axes with twinx()
import matplotlib.pyplot as plt
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
revenue = [120, 132, 150, 161, 175, 190, 210, 205, 188, 170, 160, 230] # thousand USD
customers = [4.2, 4.0, 3.7, 3.5, 3.6, 3.4, 3.9, 4.3, 4.6, 4.8, 5.1, 5.6] # thousand people
fig, ax1 = plt.subplots(figsize=(8, 4.5))
ax1.plot(months, revenue, color="tab:blue", marker="o", label="Revenue")
ax1.set_ylabel("Revenue (thousand USD)", color="tab:blue")
ax1.tick_params(axis="y", labelcolor="tab:blue")
ax2 = ax1.twinx() # second y-axis on the right, same x-axis
ax2.plot(months, customers, color="tab:red", marker="s", label="Customers")
ax2.set_ylabel("Customers (thousands)", color="tab:red")
ax2.tick_params(axis="y", labelcolor="tab:red")
lines = ax1.get_lines() + ax2.get_lines() # one legend for both axes
ax1.legend(lines, [l.get_label() for l in lines], loc="upper left")
plt.tight_layout()
plt.show()

twinx() returns a completely new axes object placed on top of the first one. Everything you do to the right axis goes through ax2:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
fig, ax1 = plt.subplots()
ax1.plot([1, 2, 3], [100, 200, 300])
ax2 = ax1.twinx()
ax2.plot([1, 2, 3], [0.1, 0.5, 0.2])
print("axes in the figure:", len(fig.axes))
print("ax1 y limits:", [round(float(v), 2) for v in ax1.get_ylim()])
print("ax2 y limits:", [round(float(v), 2) for v in ax2.get_ylim()])
print("shared x limits:", ax1.get_xlim() == ax2.get_xlim())
print("ax2 y-axis side:", ax2.yaxis.get_label_position())
Output:
axes in the figure: 2
ax1 y limits: [90.0, 310.0]
ax2 y limits: [0.08, 0.52]
shared x limits: True
ax2 y-axis side: right

Because each axes has its own legend, collect the lines from both (as above) or use fig.legend() to get one combined legend.
secondary_yaxis(): the same data in another unit
twinx() is for a second data series. When the right axis is only a different unit for the same values, secondary_yaxis() with a pair of conversion functions keeps both axes in sync automatically, even when you zoom:
import matplotlib.pyplot as plt
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
temp_c = [-2, 1, 6, 12, 18, 23]
fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(months, temp_c, marker="o")
ax.set_ylabel("°C")
# a secondary axis that shows the SAME data in another unit
secax = ax.secondary_yaxis("right", functions=(lambda c: c * 9 / 5 + 32, lambda f: (f - 32) * 5 / 9))
secax.set_ylabel("°F")
ax.set_title("secondary_yaxis: Celsius and Fahrenheit")
plt.tight_layout()
plt.show()

secondary_yaxis("right", functions=(to_f, to_c)).Bar chart and line chart with two y-axes
import matplotlib.pyplot as plt
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
rainfall = [78, 60, 70, 65, 80, 95, 110, 105, 90, 85, 88, 82] # mm
temperature = [1, 2, 7, 13, 18, 23, 26, 25, 21, 14, 8, 3] # °C
fig, ax1 = plt.subplots(figsize=(8, 4.5))
ax1.bar(months, rainfall, color="lightsteelblue", label="Rainfall (mm)")
ax1.set_ylabel("Rainfall (mm)")
ax2 = ax1.twinx()
ax2.plot(months, temperature, color="tab:orange", marker="o", linewidth=2, label="Temperature (°C)")
ax2.set_ylabel("Temperature (°C)")
fig.legend(loc="upper left", bbox_to_anchor=(0.1, 0.9))
plt.tight_layout()
plt.show()

Three or more y-axes
Create another twinx() and move its spine outward so the axes do not overlap. Leave space on the right with subplots_adjust(right=...):
import matplotlib.pyplot as plt
import numpy as np
hours = np.arange(0, 24)
temp = 15 + 8 * np.sin((hours - 8) / 24 * 2 * np.pi)
humidity = 70 - 20 * np.sin((hours - 8) / 24 * 2 * np.pi)
wind = 5 + 3 * np.cos(hours / 24 * 2 * np.pi)
fig, ax1 = plt.subplots(figsize=(9, 4.5))
fig.subplots_adjust(right=0.78)
ax2 = ax1.twinx()
ax3 = ax1.twinx()
ax3.spines["right"].set_position(("axes", 1.15)) # move the third axis further right
p1, = ax1.plot(hours, temp, color="tab:red", label="Temperature (°C)")
p2, = ax2.plot(hours, humidity, color="tab:blue", label="Humidity (%)")
p3, = ax3.plot(hours, wind, color="tab:green", label="Wind (m/s)")
for ax, p in [(ax1, p1), (ax2, p2), (ax3, p3)]:
ax.set_ylabel(p.get_label(), color=p.get_color())
ax.tick_params(axis="y", colors=p.get_color())
ax1.set_xlabel("Hour of day")
plt.show()

spines["right"].set_position(("axes", 1.15)).Secondary y-axis with pandas
DataFrame.plot(secondary_y=...) puts the listed columns on a right-hand axis, available afterwards as ax.right_ax:
import matplotlib.pyplot as plt
import pandas as pd
df = pd.DataFrame({"visits": [1200, 1350, 1600, 1580, 1900, 2250],
"conversion_rate": [2.1, 2.4, 2.2, 2.9, 3.1, 3.4]},
index=["Jan", "Feb", "Mar", "Apr", "May", "Jun"])
ax = df.plot(secondary_y="conversion_rate", marker="o", figsize=(7, 4)) # right axis for this column
ax.set_ylabel("Visits")
ax.right_ax.set_ylabel("Conversion rate (%)")
plt.tight_layout()
plt.show()

df.plot(secondary_y="conversion_rate").Secondary y-axis in subplots
Call twinx() on each subplot that needs a second axis:
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(1, 13)
stores = {"Store A": (x * 10 + 50, 34 + 4 * np.sin(x / 2)), "Store B": (x * 7 + 80, 45 - x)}
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
for ax, (name, (sales, staff)) in zip(axes, stores.items()):
ax.plot(x, sales, color="tab:blue")
ax.set_ylabel("Sales", color="tab:blue")
right = ax.twinx() # each subplot gets its own secondary axis
right.plot(x, staff, color="tab:green", linestyle="--")
right.set_ylabel("Staff", color="tab:green")
ax.set_title(name)
ax.set_xlabel("Month")
plt.tight_layout()
plt.show()

Secondary y-axis with seaborn
Seaborn functions accept ax=, so draw one plot on ax1 and the other on ax1.twinx(). With a bar plot on the x-axis, seaborn uses positions 0, 1, 2 …, so the line uses range(len(df)) as x:
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
df = pd.DataFrame({"day": range(1, 11),
"orders": [52, 58, 61, 55, 70, 74, 69, 80, 85, 83],
"avg_basket": [31.5, 30.2, 32.8, 33.1, 29.9, 31.0, 34.2, 35.0, 33.7, 36.1]})
fig, ax1 = plt.subplots(figsize=(7, 4))
sns.barplot(data=df, x="day", y="orders", color="lightgray", ax=ax1)
ax2 = ax1.twinx() # seaborn draws on any Matplotlib axes
sns.lineplot(x=range(len(df)), y=df["avg_basket"], color="tab:purple", marker="o", ax=ax2)
ax2.set_ylabel("Average basket (USD)")
plt.tight_layout()
plt.show()

barplot and lineplot on two y-axes.Tips for charts with two y-axes
- Colo