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

| Submodule | What it does | Reach for it when |
|---|---|---|
optimize | Minimisation, curve fitting, root finding | Fitting a model to data |
stats | Distributions and hypothesis tests | Probabilities, t-tests, sampling |
integrate | Numerical integration and ODEs | Area under a curve |
interpolate | Values between known points | Filling gaps in a series |
signal | Filtering, convolution, peaks | Cleaning sensor data |
linalg | Matrix decompositions and solvers | Solving equation systems |
ndimage | Multi-dimensional image processing | Blurring or rotating arrays |
sparse | Memory-efficient sparse matrices | Mostly-zero matrices |
spatial | Distances, KD-trees, convex hulls | Nearest-neighbour work |
fft | Fast Fourier transforms | Moving 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()

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

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

scipy.linalg against 33 in numpy.linalg.| NumPy | SciPy | |
|---|---|---|
| Provides | The ndarray type | Algorithms that use it |
| Linear algebra | The essentials | Far more, including decompositions |
| Statistics | Mean, median, std | Distributions and hypothesis tests |
| Optimisation | None | A whole submodule |
| Depends on | Nothing | NumPy |
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]

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

simps is gone, and scipy.misc now contains nothing.| Old code | Status | Use instead |
|---|---|---|
integrate.simps | Removed | integrate.simpson |
scipy.misc.imread | Removed | imageio or PIL |
scipy.misc.face | Moved | scipy.datasets.face |
scipy.misc | Deprecated, empty | Nothing, it is going in 2.0 |
basey= on log scales | Renamed | base= (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:
- Optimisation with scipy.optimize.minimize
- Curve fitting with curve_fit
- The normal distribution in scipy.stats
- Filter frequency response with signal.freqz
- Finding peaks in a signal
- Linear programming with linprog
- Numerical integration
- Interpolation
- The scipy.stats module
- Butterworth filter design
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.
