Without it, the reader has two scales and no way to tell which belongs to which line, and the chart is simply unreadable.
Worth knowing
ax.twinx() returns a new axes sharing the x axis; colour each y label and its ticks to match its series.
Each axes owns its artists, so one legend needs the handles collected and passed explicitly.
The point where two twinned series cross is an artefact of the limits you chose, and readers read it as a fact.
The honest alternatives are stacked panels with a shared x, or indexing both series to a common base and using one axis.
secondary_yaxis(functions=(fwd, inv)) relabels the same data in another unit — no arbitrary scale, nothing to mislead.
A twin axis is defensible when one series is clearly background context — volume bars behind a price line.
Twin and Secondary Axes
Two scales on one plot, and the reasons to think twice.
twinx shares the x axis
A second axes on top of the first, with its own y scale.
example_01.pymatplotlib
Output
The legend problem
Each axes has its own artists, so a single legend needs assembling.
example_02.pymatplotlib
Output
ax.legend() collects labelled artists on that axes only. With a twin, that means each half of the chart gets its own legend, which looks like a mistake.
Or gather them generically with ax.get_legend_handles_labels() on each axes and concatenate.
Why twin axes mislead
The crossing point is an artefact of the two scales you chose.
example_03.pymatplotlib
Output
The usual alternatives
Two panels, or one scale after normalising.
example_04.pymatplotlib
Output
secondary_axis is a different thing
One scale shown in two units, with a conversion between them.
example_05.pymatplotlib
Output
When a twin axis is reasonable
Related quantities, honestly labelled, where the shape is the point.
example_06.pymatplotlib
Output
Why they mislead
This is the part worth taking seriously.
Two series on independent scales have no defined relationship. Where they cross, which is above which, whether they diverge — all of it depends on the limits you chose for the second axis. Change ax2.set_ylim and the crossing moves, or disappears.
The third editor draws the same two series three times with different right-hand limits, and they cross in three different places.
Readers do not see it that way. A crossing looks like an event: "revenue overtook cost in June". It is an artefact of a choice you made, usually without thinking about it, because matplotlib picked the limits automatically.
This is the mechanism behind a large share of misleading charts in the wild, and most of them are not deliberate.
The alternatives
Stacked panels with sharex=True. Two small charts, one above the other, sharing the time axis. The shapes can be compared, the levels cannot be confused, and no crossing is implied. This is the right answer most of the time.
Index both to a common base. Divide each series by its first value and multiply by 100. Both are then percentages of their own starting point, they share one axis honestly, and a crossing now means something real: relative growth diverged.
Percentage change does the same job for series where the level is not meaningful.
Two charts side by side, when the series have nothing in common but the time axis.
secondary_axis
ax.secondary_yaxis("right", functions=(forward, inverse)) is a different mechanism and does not share the drawbacks.
It shows the same data in a second unit, with an explicit conversion. Celsius and Fahrenheit, millimetres and inches, a count and a percentage of a known total.
Because the two scales are related by a function rather than by an arbitrary choice, nothing about the chart can mislead: the line is in one place, and the two axes are two ways of reading the same position.
The functions must be inverses of each other, and matplotlib uses both — forward to place the ticks, inverse to keep them consistent when the view changes.
secondary_xaxis does the same horizontally, which is how you put dates on one side and elapsed days on the other.
When a twin axis is fine
There is a defensible case, and it is narrower than its popularity suggests.
It works when one series is clearly background context rather than a co-equal subject. The standard example is price and volume: volume as pale bars pushed to the bottom of the chart, price as a line in front. Nobody tries to compare a bar height with a line position, because the visual hierarchy makes clear which one is the subject.
Setting the bar axis limit to two or three times the data range pushes the bars down and reinforces that.
The test: if a reader might compare the two series against each other, the twin axis is a hazard. If one is obviously scenery, it is a reasonable use of space.
Ordering and z-order
The twin axes is created after the original, so its artists draw on top by default.
That is usually wrong when the first series is the subject: a background bar series drawn on ax ends up behind, which is right, but a line drawn on ax is covered by anything on ax2.
ax.set_zorder(ax2.get_zorder() + 1) moves the first axes in front, and ax.patch.set_visible(False) is then required, or its opaque background hides the second axes entirely.
That two-line incantation appears in a great deal of twin-axis code, and it exists because the axes are genuinely stacked rather than merged.
Grid lines on a twin
Both axes can draw a grid, and two grids at different spacings is visual noise that makes both harder to read.
The convention is to grid only the axes whose scale the reader should use for judging values — usually the primary one — and to turn the other off:
Whether that is a good idea is another question. Aligning zero makes the chart look more comparable without making it more comparable — the scales are still unrelated, and the reader is now more likely to compare them.
Three axes
ax.twinx() twice gives a third scale, with the third spine pushed outward:
It works, and it is almost always a mistake. Three unrelated scales on one plot is three arbitrary choices, and no amount of colour coding makes the crossings meaningful.
If three series must be shown together, three stacked panels sharing an x axis is the display that does it honestly, and it is not much taller.
The honest summary
Twin axes exist because people want them, and matplotlib provides them without editorialising.
They are appropriate when one series is context and the other is the subject, when the two quantities are related by something the reader knows, or when a convention in the field makes the pairing familiar.
They are inappropriate when the chart invites a comparison of levels or a reading of the crossing point — which is most of the time, and is why the alternatives in this module are worth reaching for first.
How the misreading happens
Worth spelling out, because it is not obvious that anything is wrong.
A reader sees two lines. Lines on a chart normally share a scale, so distance between them means something. Here it does not: the vertical gap is the difference between two arbitrary rescalings.
The crossing is worse. A crossing is a visually salient event, and the reader interprets it as "the quantities became equal" — which is meaningless when the units differ. Move one axis's limits and the crossing moves, or vanishes.
None of that is signalled. The chart looks like every other two-line chart, and the two y axes are the only clue that the usual reading does not apply.
If you must
When a twin axis is genuinely the right choice, four things make it as safe as it can be.
Colour-code both axes to their series, including the tick labels.
Make the hierarchy obvious — one series as background context, the other as the subject.
Do not let them cross, if the limits can reasonably be set to avoid it.
Say in the caption that the scales are independent.
And check the alternative honestly first. Most twin-axis charts exist because two series were to hand, not because the comparison needed one plot.
A decision procedure
When a second scale is proposed, four questions settle it.
Are the units related by a known conversion? If yes, secondary_yaxis with the conversion functions — safe, and not really a second series at all.
Is one series clearly background? If yes, a twin axis is defensible, with the background pushed down and drawn in a pale colour.
Would indexing both to a common base answer the question? Usually yes, and it puts them on one honest axis where crossings mean something.
Would two stacked panels do? Almost always yes, and it costs nothing but vertical space.
Only if the first two are no and the second two are genuinely unsuitable is a co-equal twin axis the right answer — and by that point the reasoning has been made explicit, which is the useful part of the exercise.
In summary
twinx gives a second axes sharing the x axis and its own y scale.
Colour-code both axes to their series, or the chart is unreadable.
One legend requires collecting the handles from both axes.
The crossing point is determined by the limits you chose, and readers treat it as a finding — which is the whole problem.
secondary_yaxis with a function pair is a different and safe mechanism, relabelling one series in another unit.
The honest alternatives are stacked panels sharing an x axis, or indexing both series to a common base.
And a twin axis is reasonable when one series is obviously scenery — volume behind price — because then nobody is trying to compare them.
A worked alternative
Given revenue in thousands and conversion rate in percent over twelve months, the twin-axis version puts them on one plot and invites a comparison that is not defined.
Three alternatives, each answering a slightly different question.
Stacked panels, sharing the x axis. Answers "how did each move?" and allows the shapes to be compared without implying anything about levels.
Indexed to January = 100. Answers "which grew faster?" and puts both on one honest axis, where a crossing genuinely means relative performance diverged.
A scatter of one against the other, coloured or labelled by month. Answers "are they related?" directly, which is usually the underlying question when someone reaches for a twin axis.
That third one is worth considering more often. A twin-axis chart is frequently an attempt to show a relationship using a display designed for showing two time series, and a scatter shows the relationship.
One more thing
ax2.set_ylim should generally be set explicitly on a twin axis rather than left automatic.
The reason is reproducibility rather than aesthetics: automatic limits depend on the data, so a chart regenerated next month has a different second scale and the two lines cross somewhere new. If a reader has seen the earlier version, that looks like a change in the data.
The short version
A second y axis is the only common chart feature whose main effect is on what the reader wrongly concludes.
Everything else in this track can be done badly; a twin axis is difficult to do well even deliberately, because the ambiguity is in the display rather than in the execution. Reaching for stacked panels or a common index first is the habit worth having.
Reading the code back
A twin axis is one call and a series of mitigations: colour-code both scales, assemble one legend, fix the limits explicitly, establish a visual hierarchy, and say in the caption that the scales are independent. The length of that list is itself the argument for checking whether stacked panels would do instead.
Check yourself
0 of 4
Answer without scrolling back up.
Why must you colour-code the two y axes on a twin plot?
Set both the label colour and tick_params labelcolor. Without it the chart is simply unreadable.
What determines where two twinned series cross?
Readers see a crossing as an event in the data. It is an artefact of a choice usually made automatically.
What is the honest alternative to a twin axis?
Indexing puts them on one axis honestly, and then a crossing means something real - relative growth diverged.
How does `secondary_yaxis` differ from `twinx`?
Celsius/Fahrenheit or mm/inches - the two scales are related by a function, so nothing can mislead.
Cheat sheet
Twin and Secondary Axes
ax2 = ax.twinx() creates a second axes occupying the same space, sharing the x axis and having its own y axis on the right. twiny() is the transpose.
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.