SciPy in Python: What It Is and How to Use It

SciPy is an open-source Python library for scientific and technical computing. It sits on top of NumPy and adds the algorithms NumPy deliberately leaves out:

  • Optimization — curve fitting, finding minima, solving equations.
  • Integration — areas under curves and differential equations.
  • Statistics — distributions, hypothesis tests, descriptive stats.
  • Signal and image processing — filters, convolution, peak finding.
  • Linear algebra — a much larger set of routines than NumPy’s.

NumPy gives you the array. SciPy gives you the things you do to the array.

The plots and output below are real runs on Python 3.12.5, SciPy 1.18.1, NumPy 2.5.3.

Installing SciPy and checking the version

One command, and it brings NumPy with it:

pip install scipy

# or, in a conda environment
conda install scipy
import scipy
import numpy

print("SciPy version:", scipy.__version__)
print("NumPy version:", numpy.__version__)

# SciPy is built on NumPy, so installing it pulls NumPy in too
print("\nSciPy needs NumPy:", scipy.__version__ is not None and numpy.__version__ is not None)

Output:

SciPy version: 1.18.1
NumPy version: 2.5.3

SciPy needs NumPy: True

If the import fails with ModuleNotFoundError, you almost certainly installed it into a different interpreter from the one running your script.

What is in SciPy: the submodules

SciPy is not one flat namespace. It’s a collection of specialised packages, and knowing which one you need is most of the battle:

import pkgutil
import scipy

names = sorted(m.name for m in pkgutil.iter_modules(scipy.__path__)
               if not m.name.startswith("_") and m.name not in {"conftest", "version"})

print(f"SciPy ships {len(names)} public submodules:\n")
for i in range(0, len(names), 4):
    print("  " + "".join(n.ljust(16) for n in names[i:i + 4]))

Output:

SciPy ships 19 public submodules:

  cluster         constants       datasets        differentiate   
  fft             fftpack         integrate       interpolate     
  io              linalg          misc            ndimage         
  odr             optimize        signal          sparse          
  spatial         special         stats
Command Prompt listing all of the SciPy submodules available in version 1.18
The full set. You will use five or six of these regularly.
SubmoduleWhat it doesReach for it when
optimizeMinimisation, curve fitting, root findingFitting a model to data
statsDistributions and hypothesis testsProbabilities, t-tests, sampling
integrateNumerical integration and ODEsArea under a curve
interpolateValues between known pointsFilling gaps in a series
signalFiltering, convolution, peaksCleaning sensor data
linalgMatrix decompositions and solversSolving equation systems
ndimageMulti-dimensional image processingBlurring or rotating arrays
sparseMemory-efficient sparse matricesMostly-zero matrices
spatialDistances, KD-trees, convex hullsNearest-neighbour work
fftFast Fourier transformsMoving to the frequency domain

What SciPy can do, in four plots

Rather than describe the submodules, here are four of them working on real data:

import numpy as np
import matplotlib.pyplot as plt
from scipy import integrate, stats, signal, interpolate

fig, axes = plt.subplots(2, 2, figsize=(11, 7))

# 1. integrate: shade the area under a curve
x = np.linspace(0, np.pi, 300)
axes[0, 0].plot(x, np.sin(x), color="#0b6bcb", lw=2)
axes[0, 0].fill_between(x, np.sin(x), alpha=.3, color="#0b6bcb")
area, _ = integrate.quad(np.sin, 0, np.pi)
axes[0, 0].set_title(f"integrate.quad: area = {area:.2f}")

# 2. stats: a fitted normal distribution
data = stats.norm.rvs(loc=5, scale=1.5, size=2000, random_state=0)
axes[0, 1].hist(data, bins=40, density=True, alpha=.6, color="#0a7d32")
grid = np.linspace(data.min(), data.max(), 200)
mu, sd = stats.norm.fit(data)
axes[0, 1].plot(grid, stats.norm.pdf(grid, mu, sd), color="#c0392b", lw=2)
axes[0, 1].set_title(f"stats.norm.fit: mean {mu:.2f}, sd {sd:.2f}")

# 3. signal: smooth a noisy measurement
rng = np.random.default_rng(1)
noisy = np.sin(np.linspace(0, 4 * np.pi, 300)) + rng.normal(0, .4, 300)
smooth = signal.savgol_filter(noisy, 31, 3)
axes[1, 0].plot(noisy, color="#bbbbbb", lw=1, label="raw")
axes[1, 0].plot(smooth, color="#8e44ad", lw=2, label="savgol_filter")
axes[1, 0].set_title("signal.savgol_filter"); axes[1, 0].legend()

# 4. interpolate: a smooth curve through few points
px, py = np.array([0, 1, 2, 3, 4]), np.array([0, 2, 1, 3, 2])
fine = np.linspace(0, 4, 300)
axes[1, 1].plot(px, py, "o", color="#c0392b", markersize=9, label="known points")
axes[1, 1].plot(fine, interpolate.interp1d(px, py, kind="cubic")(fine),
                color="#0b6bcb", lw=2, label="cubic interp1d")
axes[1, 1].set_title("interpolate.interp1d"); axes[1, 1].legend()

for ax in axes.ravel(): ax.grid(alpha=.3)
plt.tight_layout(); plt.show()
Four SciPy plots showing integration area, a fitted normal distribution, signal smoothing and cubic interpolation
Integration, statistics, signal smoothing and interpolation, each in about four lines.

That is the character of the library. Each of those results is a single function call against a NumPy array.

A quick tour of the SciPy submodules in code

The same five submodules, printed rather than plotted, so you can see the exact calls:

import numpy as np
from scipy import integrate, stats, signal, interpolate, linalg

# integrate: the area under a curve
area, _ = integrate.quad(lambda x: np.sin(x), 0, np.pi)
print("integrate.quad  sin(x) from 0 to pi :", round(area, 6), "(exactly 2)")

# stats: a probability from a normal distribution
print("stats.norm.cdf  P(x < 1.96)         :", round(stats.norm.cdf(1.96), 6))

# interpolate: a value between known points
f = interpolate.interp1d([0, 1, 2, 3], [0, 1, 4, 9], kind="quadratic")
print("interpolate     value at x = 1.5    :", round(float(f(1.5)), 6))

# linalg: solve a system of equations
solution = linalg.solve(np.array([[3, 1], [1, 2]]), np.array([9, 8]))
print("linalg.solve    3x+y=9, x+2y=8      :", solution)

# signal: find peaks in a wave
peaks, _ = signal.find_peaks(np.sin(np.linspace(0, 6 * np.pi, 500)))
print("signal.find_peaks  peaks in 3 cycles:", len(peaks))

Output:

integrate.quad  sin(x) from 0 to pi : 2.0 (exactly 2)
stats.norm.cdf  P(x < 1.96)         : 0.975002
interpolate     value at x = 1.5    : 2.25
linalg.solve    3x+y=9, x+2y=8      : [2. 3.]
signal.find_peaks  peaks in 3 cycles: 3
Command Prompt showing SciPy integrate, stats, interpolate, linalg and signal calls with their results
quad returns exactly 2 for the integral of sin from 0 to π.

Each line is the entry point to a much bigger area. The integration result is exact, the statistics call gives the familiar 0.975, and find_peaks located all three peaks.

SciPy vs NumPy: what is the difference?

They are not competitors. NumPy provides the array type; SciPy provides algorithms that operate on it:

import numpy as np
import numpy.linalg as nla
from scipy import linalg as sla

public = lambda m: [n for n in dir(m) if not n.startswith("_")]

print("numpy.linalg public names:", len(public(nla)))
print("scipy.linalg public names:", len(public(sla)))

print()
# both solve the same system, and SciPy adds far more specialised routines
A_matrix = np.array([[3.0, 1.0], [1.0, 2.0]])
b = np.array([9.0, 8.0])
print("numpy.linalg.solve:", nla.solve(A_matrix, b))
print("scipy.linalg.solve:", sla.solve(A_matrix, b))

print()
extras = sorted(set(public(sla)) - set(public(nla)))
print("in scipy.linalg only, for example:")
print(" ", ", ".join(extras[:10]))

Output:

numpy.linalg public names: 33
scipy.linalg public names: 113

numpy.linalg.solve: [2. 3.]
scipy.linalg.solve: [2. 3.]

in scipy.linalg only, for example:
  LinAlgWarning, bandwidth, basic, blas, block_diag, cdf2rdf, cho_factor, cho_solve, cho_solve_banded, cholesky_banded
Command Prompt comparing the number of public functions in scipy.linalg against numpy.linalg
113 public names in scipy.linalg against 33 in numpy.linalg.
NumPySciPy
ProvidesThe ndarray typeAlgorithms that use it
Linear algebraThe essentialsFar more, including decompositions
StatisticsMean, median, stdDistributions and hypothesis tests
OptimisationNoneA whole submodule
Depends onNothingNumPy

Where both offer the same function, scipy.linalg is generally the one to prefer. It is always compiled against optimised LAPACK, which numpy.linalg is not guaranteed to be.

So the rule is simple: use NumPy for arrays and their shapes, and SciPy the moment you need an actual algorithm.

Do you need to import SciPy submodules?

Most guides insist you must. On current versions that is no longer true:

import scipy

# older guides say you must import each submodule first. Modern SciPy loads them on demand:
print("scipy.optimize.minimize ->", scipy.optimize.minimize.__name__)
print("scipy.stats.norm        ->", type(scipy.stats.norm).__name__)

print()
# the explicit form still reads better and works on every version
from scipy import optimize, stats
print("explicit import works too:", optimize.minimize.__name__, "|", stats.norm.mean())

Output:

scipy.optimize.minimize -> minimize
scipy.stats.norm        -> norm_gen

explicit import works too: minimize | 0.0

Modern SciPy loads submodules on demand, so scipy.optimize.minimize resolves after nothing more than import scipy.

Write the explicit import anyway. from scipy import optimize states the dependency, works on every version, and is what readers of your code expect.

Fitting a curve with scipy.optimize

Curve fitting is the single most common reason people install SciPy, so it’s worth a full example:

import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit

# measured data with noise
rng = np.random.default_rng(0)
x = np.linspace(0, 10, 40)
true_a, true_b = 2.5, 0.6
y = true_a * np.exp(-true_b * x) + rng.normal(0, 0.08, x.size)

def model(x, a, b):
    return a * np.exp(-b * x)

params, _ = curve_fit(model, x, y, p0=[1, 1])

print("true values :", true_a, true_b)
print("fitted      :", params.round(4))

plt.figure(figsize=(7.5, 4.2))
plt.plot(x, y, "o", color="#c0392b", label="noisy data")
plt.plot(x, model(x, *params), color="#0b6bcb", lw=2, label="curve_fit result")
plt.title("scipy.optimize.curve_fit recovers the parameters")
plt.legend(); plt.grid(alpha=.3); plt.tight_layout()
plt.show()

Output:

true values : 2.5 0.6
fitted      : [2.5395 0.6217]
A SciPy curve_fit result plotted over noisy exponential data
Forty noisy points, and curve_fit recovers the original parameters.

You supply the shape of the model as a function and a starting guess. SciPy finds the parameter values that fit the data best.

p0 matters more than it looks. A poor starting guess can leave the fit stuck, which is the same issue covered in scipy.optimize.minimize.

SciPy changes that break old tutorials

SciPy removes deprecated names on a schedule, so code from a few years ago often fails:

import warnings
from scipy import integrate

# 1. simps was renamed to simpson
try:
    integrate.simps([1, 2, 3])
except AttributeError as err:
    print("integrate.simps ->", type(err).__name__ + ":", err)

print("integrate.simpson works:", integrate.simpson([1, 2, 3]))

print()
# 2. scipy.misc is deprecated and now empty
with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    import scipy.misc
    for w in caught:
        print("importing scipy.misc ->", w.message)

print("scipy.misc contents now:", [n for n in dir(scipy.misc) if not n.startswith("_")])
print("scipy.misc.imread still there?", hasattr(scipy.misc, "imread"))

Output:

integrate.simps -> AttributeError: module 'scipy.integrate' has no attribute 'simps'
integrate.simpson works: 4.0

importing scipy.misc -> scipy.misc is deprecated and will be removed in 2.0.0
scipy.misc contents now: ['warnings']
scipy.misc.imread still there? False
Command Prompt showing scipy integrate simps raising an AttributeError and scipy misc reported as deprecated and empty
simps is gone, and scipy.misc now contains nothing.
Old codeStatusUse instead
integrate.simpsRemovedintegrate.simpson
scipy.misc.imreadRemovedimageio or PIL
scipy.misc.faceMovedscipy.datasets.face
scipy.miscDeprecated, emptyNothing, it is going in 2.0
basey= on log scalesRenamedbase= (that one is Matplotlib)

If a SciPy example from a blog post raises AttributeError, check the release notes before assuming your install is broken. The name has usually just moved.

Where to go next with SciPy

The submodules each deserve their own guide, and these are the ones people reach for first:

Frequently asked questions

What is SciPy in Python?

An open-source library for scientific computing, built on NumPy. It supplies optimisation, integration, statistics, signal processing and linear algebra routines. The project is documented at the SciPy documentation.

What is the difference between NumPy and SciPy?

NumPy provides the array type and basic operations. SciPy provides algorithms that work on those arrays. SciPy depends on NumPy, not the other way round.

How do I install SciPy?

pip install scipy, which installs NumPy as a dependency. Check it with import scipy; print(scipy.__version__).

Do I have to import SciPy submodules separately?

Not on current versions, which load them on demand. Writing from scipy import optimize is still clearer and works on every version.