Matplotlib sets a log scale with one call: plt.loglog() for both axes, plt.semilogx() or plt.semilogy() for one, or ax.set_xscale("log") on a plot you’ve already drawn:
import matplotlib.pyplot as plt
plt.loglog(x, y) # both axes
plt.semilogy(x, y) # y axis only
ax.set_xscale("log") # switch an existing axis
This guide covers log-log plots, power-law fits, the base, zeros, error bars and histograms. The charts are real windows from Python 3.14.7, Matplotlib 3.11.2 and NumPy 2.5.3.
Set a log scale on the x axis, y axis or both
Here’s the same data, y = x², with each of the three options side by side:
import matplotlib.pyplot as plt
import numpy as np
x = np.logspace(0, 3, 40) # 1 to 1,000
y = x ** 2
fig, axes = plt.subplots(1, 3, figsize=(12, 3.8), layout="constrained")
axes[0].semilogx(x, y); axes[0].set_title("semilogx: log x axis")
axes[1].semilogy(x, y); axes[1].set_title("semilogy: log y axis")
axes[2].loglog(x, y); axes[2].set_title("loglog: both axes")
for ax in axes:
ax.grid(True, which="both", alpha=0.3)
plt.show()
for ax in axes:
print(f"{ax.get_title():<22} x={ax.get_xscale():<7} y={ax.get_yscale()}")
Output:
semilogx: log x axis x=log y=linear
semilogy: log y axis x=linear y=log
loglog: both axes x=log y=log

y = x² into a straight line.semilogx logs the x axis, semilogy logs the y axis, and loglog logs both. They’re shortcuts for plot() followed by set_xscale and set_yscale.
Which one you want depends on the data. Exponential growth is straight on semilogy, while a power law like x² is straight only on loglog.
When only the y axis needs a log scale, setting the Matplotlib y axis to log scale goes deeper, including symlog for data that crosses zero.
Log scale in a pandas plot
If your data is in a DataFrame, you don’t need Matplotlib calls at all. df.plot() takes the log switches directly:
import matplotlib
matplotlib.use("Agg")
import numpy as np
import pandas as pd
days = pd.date_range("2026-01-01", periods=6, freq="MS")
signups = pd.DataFrame({"signups": [120, 480, 1900, 7600, 30000, 121000]}, index=days)
for kwargs in ({"logy": True}, {"logx": True}, {"loglog": True}):
ax = signups.plot(**kwargs)
print(f"df.plot({list(kwargs)[0]}=True) -> x {ax.get_xscale():<7} y {ax.get_yscale()}")
Output:
df.plot(logy=True) -> x linear y log
df.plot(logx=True) -> x log y linear
df.plot(loglog=True) -> x log y log
logy=True suits growth data like these monthly sign-ups, which multiply by about four each month. On a log y axis that steady multiplication becomes a straight line you can actually read.
Make a log-log plot with plt.loglog
plt.loglog() takes the same arguments as plt.plot(), so markers, labels and colors all work as usual:
import matplotlib.pyplot as plt
import numpy as np
requests = np.array([1, 5, 10, 50, 100, 500, 1000, 5000])
latency_ms = 3.0 * requests ** 0.8
lines = plt.loglog(requests, latency_ms, marker="o", label="API server")
plt.xlabel("Requests per second")
plt.ylabel("Latency (ms)")
plt.title("Latency vs load, 2026 benchmark")
plt.legend()
plt.show()
print("plt.loglog returned:", type(lines).__name__, "of", type(lines[0]).__name__)
Output:
plt.loglog returned: list of Line2D

Like plot(), it returns a list of line objects, one per line drawn. ax.loglog() is the object-oriented version and does exactly the same on a specific axes.
On linear axes, the small request counts would be squashed into one corner. The log scale gives each factor of ten the same amount of space.
Why a power law is a straight line on a log-log plot
That straight line isn’t a coincidence, and it’s the main reason scientists and engineers reach for log-log plots:
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(42)
x = np.logspace(0, 3, 30)
y = 3.0 * x ** 1.5 * rng.lognormal(0, 0.08, x.size)
slope, intercept = np.polyfit(np.log10(x), np.log10(y), 1)
print(f"fitted exponent: {slope:.3f} (true value 1.5)")
print(f"fitted coefficient: {10 ** intercept:.3f} (true value 3.0)")
fig, ax = plt.subplots(figsize=(7, 4.5), layout="constrained")
ax.loglog(x, y, "o", label="measurements")
ax.loglog(x, 10 ** intercept * x ** slope, "-", label=f"fit: y = {10 ** intercept:.2f} x^{slope:.2f}")
ax.grid(True, which="both", alpha=0.3)
ax.legend()
plt.show()
Output:
fitted exponent: 1.506 (true value 1.5)
fitted coefficient: 2.947 (true value 3.0)

Taking the log of y = a · xb gives log y = log a + b · log x, which is a straight line. So fitting a line to the logs recovers the exponent as the slope.
np.polyfit on log10 of both arrays did exactly that here, landing very close to the true exponent of 1.5. I use this constantly for scaling tests: plot, fit, read the slope.
Change the log base in Matplotlib
Base 10 is the default. For data that doubles, like memory sizes, base 2 reads much better:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
memory_mb = [1, 2, 4, 8, 16, 32, 64, 128]
fig, ax = plt.subplots()
ax.plot(memory_mb, range(len(memory_mb)), "o-")
ax.set_xscale("log", base=2)
fig.canvas.draw()
labels = [t.get_text() for t in ax.get_xticklabels()][1:-1]
print("tick labels:", [s.replace("$\\mathdefault{", "").replace("}$", "").replace("{", "").replace("}", "") for s in labels])
Output:
tick labels: ['2^0', '2^1', '2^2', '2^3', '2^4', '2^5', '2^6', '2^7']
set_xscale("log", base=2) labels the ticks as powers of two. loglog() and semilogx() accept the same base argument.
Older tutorials use basex=2, which current Matplotlib rejects with a TypeError. The next section shows the exact message.
What happens to zero and negative values on a log scale?
A log scale can’t show zero or negative numbers, since their logarithm doesn’t exist. What Matplotlib does about it surprises people:
import warnings
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
def try_plot(label, xs):
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
fig, ax = plt.subplots()
ax.plot(xs, range(len(xs)), "o")
ax.set_xscale("log")
fig.canvas.draw()
low, high = ax.get_xlim()
print(f"{label:<28} x range {low:.2f} to {high:.2f} | warnings: {len(caught)}")
for w in caught[:1]:
print(" ", w.message)
plt.close(fig)
try_plot("[0, 1, 10, 100]", [0, 1, 10, 100])
try_plot("[-5, 1, 10, 100]", [-5, 1, 10, 100])
try_plot("[0, -1, -10] (none positive)", [0, -1, -10])
print()
fig, ax = plt.subplots()
for kwargs in ({"basex": 2}, {"base": 1}):
try:
ax.set_xscale("log", **kwargs)
except (TypeError, ValueError) as err:
print(f"set_xscale('log', {kwargs}) -> {type(err).__name__}: {err}")
Output:
[0, 1, 10, 100] x range 0.79 to 125.89 | warnings: 0
[-5, 1, 10, 100] x range 0.79 to 125.89 | warnings: 0
[0, -1, -10] (none positive) x range 0.89 to 11.22 | warnings: 2
Data has no positive values, and therefore cannot be log-scaled.
set_xscale('log', {'basex': 2}) -> TypeError: LogScale.__init__() got an unexpected keyword argument 'basex'. Did you mean 'base'?
set_xscale('log', {'base': 1}) -> ValueError: The log base cannot be <= 0 or == 1

The zero and the -5 were simply left out. No error, no warning, just a missing point, which is easy to miss in a busy chart.
You only get a warning when every value is non-positive: Data has no positive values, and therefore cannot be log-scaled. Filter your data first if a missing point would matter.
The bottom two lines of the screenshot are the base errors: basex became base, and a base of 1 or less is refused. For data that genuinely crosses zero, symlog is covered in the y axis log scale guide.
Set axis limits on a log scale
The same rule applies when you set the limits yourself. A limit of zero can’t exist on a log axis:
import warnings
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.loglog([1, 10, 100, 1000], [1, 10, 100, 1000])
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
ax.set_xlim(0, 1000)
low, high = ax.get_xlim()
print(f"set_xlim(0, 1000) -> ({low:.2f}, {high:.0f})")
for w in caught:
print(" ", w.message)
ax.set_xlim(0.5, 2000)
low, high = ax.get_xlim()
print(f"set_xlim(0.5, 2000) -> ({low:.2f}, {high:.0f})")
Output:
set_xlim(0, 1000) -> (0.71, 1000)
Attempt to set non-positive xlim on a log-scaled axis will be ignored.
set_xlim(0.5, 2000) -> (0.50, 2000)
Matplotlib warns and ignores the zero, keeping its old lower limit instead. Pick a small positive number like 0.5 when you want the axis to start below your first point.
Log-log plot with error bars and grid lines
Error bars and grid lines both behave a little differently on log axes:
import matplotlib.pyplot as plt
import numpy as np
sizes = np.array([10, 100, 1000, 10000])
times = np.array([0.8, 6.5, 70, 690])
errors = np.array([1.0, 2.0, 15, 120]) # the first lower bar would go below zero
fig, ax = plt.subplots(figsize=(7, 4.5), layout="constrained")
ax.errorbar(sizes, times, yerr=errors, fmt="o-", capsize=4)
ax.set_xscale("log")
ax.set_yscale("log")
ax.grid(True, which="both", alpha=0.35)
ax.set_xlabel("Input size")
ax.set_ylabel("Run time (ms)")
plt.show()
print(f"lowest error bar would reach {times[0] - errors[0]:.1f} - clipped at the bottom of the axis, no warning")
Output:
lowest error bar would reach -0.2 - clipped at the bottom of the axis, no warning