Matplotlib Set Y Axis to Log Scale (base, symlog and zeros)

To set a Matplotlib y axis to a log scale, call set_yscale on the axes or plt.yscale on the current figure:

ax.set_yscale("log")          # object-oriented
plt.yscale("log")             # pyplot

ax.set_yscale("log", base=2)  # a base other than 10

Two things catch people out. The argument is base, not basey, and any zero or negative value is dropped without a warning.

Both are demonstrated below, on Python 3.12.5, Matplotlib 3.11.2.

Setting the y axis to a log scale

A log axis earns its place whenever your values span several orders of magnitude:

import matplotlib.pyplot as plt
import numpy as np

x = np.arange(1, 11)
y = 2 ** x                      # doubles every step

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.2))

ax1.plot(x, y, marker="o", color="#0b6bcb")
ax1.set_title("Linear y axis: everything is squashed")

ax2.plot(x, y, marker="o", color="#0a7d32")
ax2.set_yscale("log")           # the whole change
ax2.set_title('ax.set_yscale("log")')

for ax in (ax1, ax2):
    ax.set_xlabel("x"); ax.set_ylabel("y"); ax.grid(alpha=.3)
plt.tight_layout(); plt.show()
Two Matplotlib plots comparing exponential data on a linear y axis and a logarithmic y axis
The same doubling data. On the left the early values are invisible.

On the linear axis every point below about 200 is pressed into the bottom of the chart. On the log axis the doubling becomes a straight line.

That straightening is the real signal: exponential growth looks linear on a log axis, so a straight line tells you the growth rate is constant.

plt.yscale vs ax.set_yscale

Two spellings, same effect. The difference is what they act on:

import matplotlib.pyplot as plt

# the pyplot way, acting on the current axes
plt.plot([1, 2, 3], [10, 100, 1000])
plt.yscale("log")
print("plt.yscale('log')     -> current axes")
plt.close()

# the object-oriented way, acting on a named axes
fig, ax = plt.subplots()
ax.plot([1, 2, 3], [10, 100, 1000])
ax.set_yscale("log")
print("ax.set_yscale('log')  -> that specific axes")

print("\nboth do the same thing. Use ax.set_yscale when you have subplots.")
print("current y scale:", ax.get_yscale())

Output:

plt.yscale('log')     -> current axes
ax.set_yscale('log')  -> that specific axes

both do the same thing. Use ax.set_yscale when you have subplots.
current y scale: log
Command Prompt showing the difference between plt.yscale and ax.set_yscale in Matplotlib
plt.yscale uses the current axes. ax.set_yscale names one.
CallActs onUse when
plt.yscale("log")The current axesA single quick plot
ax.set_yscale("log")That axes objectSubplots, or anything reusable
plt.semilogy(x, y)Plots and sets the scale at onceA one-liner

With more than one subplot, always use ax.set_yscale. plt.yscale will change whichever axes happens to be current, which is rarely the one you meant. The same logic applies to setting the axis range.

The matplotlib yscale log base parameter

The default base is 10. Pass base for anything else, which matters for data that doubles rather than multiplying by ten:

import matplotlib.pyplot as plt
import numpy as np

x = np.arange(1, 11)
y = 2 ** x

fig, axes = plt.subplots(1, 3, figsize=(12, 3.8))

for ax, base in zip(axes, (10, 2, np.e)):
    ax.plot(x, y, marker="o", color="#0b6bcb")
    ax.set_yscale("log", base=base)
    label = {10: "base=10 (default)", 2: "base=2", np.e: "base=np.e"}[base]
    ax.set_title(label)
    ax.grid(alpha=.3, which="both")

plt.tight_layout(); plt.show()

print("base=2 puts a tick at every doubling: 2, 4, 8, 16, ...")
print("base=10 ticks at 1, 10, 100, 1000")

Output:

base=2 puts a tick at every doubling: 2, 4, 8, 16, ...
base=10 ticks at 1, 10, 100, 1000
Three Matplotlib log scale plots side by side using base 10, base 2 and base e
Same data, three bases. The tick positions are what change.

base=2 puts a gridline at every doubling, which suits memory sizes, cell counts and anything binary.

base=np.e gives natural log spacing. It’s less readable for most audiences, so reach for it only when the maths calls for it.

Why basey=2 raises a TypeError

If you copied a snippet from an older tutorial, this is the error you’ll hit:

import matplotlib.pyplot as plt

fig, ax = plt.subplots()

# the old argument name, still in a lot of tutorials
try:
    ax.set_yscale("log", basey=2)
except TypeError as err:
    print("basey=2 ->", type(err).__name__ + ":", err)

# the current name
ax.set_yscale("log", base=2)
print("\nbase=2  -> works")
print("y scale is now:", ax.get_yscale())

print("\nbasey was renamed to base in Matplotlib 3.3 (2020).")

Output:

basey=2 -> TypeError: LogScale.__init__() got an unexpected keyword argument 'basey'

base=2  -> works
y scale is now: log

basey was renamed to base in Matplotlib 3.3 (2020).
Command Prompt showing a TypeError because the matplotlib basey argument was renamed to base
unexpected keyword argument 'basey'. The new name is base.

Matplotlib 3.3 renamed basex and basey to a single base, because the axis is already implied by which method you call.

The same rename hit nonposx and nonposy, which are now both nonpositive.

Why do zero and negative values disappear on a log scale?

The logarithm of zero is undefined and the log of a negative number is not a real number. Matplotlib’s response is to drop those points, quietly:

import matplotlib.pyplot as plt
import numpy as np
import warnings

values = [0, 1, 10, -5, 100]

fig, ax = plt.subplots()
ax.plot(range(5), values)

with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    ax.set_yscale("log")
    fig.canvas.draw()
    print("warnings raised:", [str(w.message) for w in caught] or "NONE")

print("\ny limits chosen:", tuple(round(v, 2) for v in ax.get_ylim()))
print("the 0 and the -5 are simply not drawn, and nothing tells you")

print("\nhow many points are actually plottable?")
print(" ", sum(1 for v in values if v > 0), "of", len(values))

Output:

warnings raised: NONE

y limits chosen: (np.float64(0.79), np.float64(125.89))
the 0 and the -5 are simply not drawn, and nothing tells you

how many points are actually plottable?
  3 of 5
Command Prompt showing that Matplotlib raises no warning when a log scale silently drops zero and negative values
No warnings at all. Two of the five points are simply gone.

Read that output again: no warning is raised. Your chart renders, it looks plausible, and it is missing data.

This is the most dangerous thing on this page. If a log-scale chart looks thinner than you expected, count your points before you trust it.

  • nonpositive="clip" — the default, pushes bad values to a very small number.
  • nonpositive="mask" — removes them from the line entirely, leaving a gap.
  • Use "symlog" instead when the data genuinely crosses zero.

Using symlog for data that crosses zero

symlog is linear near zero and logarithmic beyond it, so negative values survive:

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-50, 50, 400)
y = x ** 3

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.2))

ax1.plot(x, y, color="#c0392b")
ax1.set_yscale("log")
ax1.set_title('"log": the negative half vanishes')

ax2.plot(x, y, color="#0a7d32")
ax2.set_yscale("symlog", linthresh=100)
ax2.set_title('"symlog", linthresh=100: all of it')

for ax in (ax1, ax2):
    ax.axhline(0, color="#888", lw=.8); ax.grid(alpha=.3, which="both")
plt.tight_layout(); plt.show()

print("symlog is linear between -linthresh and +linthresh, log outside it")

Output:

symlog is linear between -linthresh and +linthresh, log outside it
Two Matplotlib plots showing a cubic curve on a log scale losing its negative half and on a symlog scale keeping it
Plain log loses half the curve. symlog keeps all of it.

linthresh sets where the linear middle stops and the log part begins. Too small and the region near zero stretches out oddly.

Start with linthresh around the smallest value you care about resolving, then adjust until the shape reads correctly.

Formatting log scale tick labels

Powers of ten are readable. Awkward numbers in scientific notation are not:

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import numpy as np

x = np.arange(1, 11)
y = 2 ** x

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.2))

ax1.plot(x, y, marker="o")
ax1.set_yscale("log")
ax1.set_title("default: scientific notation")
ax1.grid(alpha=.3, which="both")

ax2.plot(x, y, marker="o")
ax2.set_yscale("log")
ax2.yaxis.set_major_formatter(ticker.ScalarFormatter())   # plain numbers
ax2.yaxis.set_minor_formatter(ticker.NullFormatter())
ax2.set_title("ScalarFormatter: plain numbers")
ax2.grid(alpha=.3, which="both")

plt.tight_layout(); plt.show()