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
which="both" draws the minor grid, and the error bar that would go below zero is clipped.The first error bar would reach below zero, which a log axis can’t show, so Matplotlib clips it at the bottom edge. Again there’s no warning, so check any bar that looks cut short.
ax.grid(True) alone draws only the major lines at 10, 100, 1000. On a log axis the minor lines are what let you read values between them, so pass which="both".
Error bars in general, including asymmetric ones, are covered in plotting error bars in Matplotlib.
Log scale histograms: log=True versus log-spaced bins
plt.hist(..., log=True) looks like the answer for a log histogram, but it isn’t always:
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(7)
order_values = rng.lognormal(mean=3, sigma=1.2, size=5000) # skewed, like real order sizes
fig, (left, right) = plt.subplots(1, 2, figsize=(11, 4), layout="constrained")
counts_log, _, _ = left.hist(order_values, bins=40, log=True)
left.set_title("hist(..., log=True): only the counts are log")
bins = np.logspace(np.log10(order_values.min()), np.log10(order_values.max()), 40)
counts_bins, _, _ = right.hist(order_values, bins=bins)
right.set_xscale("log")
right.set_title("log-spaced bins on a log x axis")
plt.show()
print("left: x", left.get_xscale(), "| y", left.get_yscale(), "| empty bins", int((counts_log == 0).sum()), "of 40")
print("right: x", right.get_xscale(), "| y", right.get_yscale(), "| empty bins", int((counts_bins == 0).sum()), "of 40")
Output:
left: x linear | y log | empty bins 26 of 40
right: x log | y linear | empty bins 3 of 40
log=True logs the counts; log-spaced bins are what you need for a log x axis.log=True only puts the counts on a log scale. The x axis stays linear, so skewed data like order values still piles up in the first few bars.
For a log x axis, build the bins with np.logspace and then call set_xscale("log"). Equal-width bins on a log axis leave most of them empty, as the empty-bin counts show.
Log-log scatter plot
Scatter plots take the same two calls. There’s no scatter version of loglog, so set both scales yourself:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(3)
population = 10 ** rng.uniform(3, 7, 60) # towns of 1,000 to 10 million people
stores = 0.002 * population ** 0.9 * rng.lognormal(0, 0.3, 60)
fig, ax = plt.subplots()
ax.scatter(population, stores, s=18)
ax.set_xscale("log")
ax.set_yscale("log")
print("scatter axes:", ax.get_xscale(), ax.get_yscale())
print("x spans", f"{population.min():,.0f} to {population.max():,.0f}")
Output:
scatter axes: log log
x spans 1,014 to 7,831,431
With populations from about a thousand to nearly eight million, a linear axis would pile nearly every town against the left edge. On log axes the whole range spreads out, and a power-law relationship shows up as a straight trend.
Minor ticks, tick labels and colorbars on a log scale
Log axes have their own tick machinery. LogLocator places the ticks, and minor ticks sometimes vanish when the axis spans many decades.
That topic has its own guide: Matplotlib log scale minor ticks and log colorbars, including LogNorm for putting a colorbar on a log scale.
plt.loglog vs semilogx vs set_xscale: which should you use?
| You want | Use |
|---|---|
| Both axes logarithmic, new plot | plt.loglog(x, y) or ax.loglog(x, y) |
| Only the x axis | ax.semilogx(x, y) |
| Only the y axis | ax.semilogy(x, y) |
| Switch an existing plot, or scatter/bar/hist | ax.set_xscale("log") / ax.set_yscale("log") |
| Base 2 instead of 10 | add base=2 |
| Data that crosses zero | set_yscale("symlog") |
I default to set_xscale and set_yscale in real projects. They work with every plot type, and they read clearly when someone else opens the code.
A few more Matplotlib guides that pair well with log scales:
- Plot multiple lines in Matplotlib
- Set the x axis label in Matplotlib
- Put the legend outside the plot
- Save a Matplotlib chart as PNG
Frequently asked questions
How do I set a log scale in Matplotlib?
Call ax.set_xscale("log") and/or ax.set_yscale("log"), or plot with plt.loglog(), plt.semilogx() or plt.semilogy().
What is the difference between plt.loglog and plt.plot?
plt.loglog() is plt.plot() with both axes switched to a log scale. It takes the same arguments.
Why is my data missing on a log scale?
Zero and negative values can’t be shown on a log axis, and Matplotlib drops them without a warning. Filter them out or use symlog.
How do I change the log base in Matplotlib?
Pass base, for example ax.set_xscale("log", base=2). The old basex and basey arguments now raise a TypeError.
How do I make a histogram with a log scale?
hist(data, log=True) logs the counts. For a log x axis, use bins=np.logspace(...) and ax.set_xscale("log").
How do I show minor grid lines on a log plot?
Use ax.grid(True, which="both"). The default draws only the major grid lines.
Why is a power law a straight line on a log-log plot?
Because log y = log a + b · log x is linear in the logs, with the exponent as the slope. Matplotlib’s own log scale examples show more variations.
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn