Use scipy.signal.freqz to calculate a digital filter’s complex frequency response, then plot np.abs(h) for magnitude or 20 * np.log10(np.abs(h)) for magnitude in decibels. Pass the sampling frequency through fs when you want the horizontal axis in hertz.
Calculate and plot the magnitude response
Here is a complete example for a filter whose numerator coefficients are in b and denominator coefficients are in a:
As an Amazon Associate I earn from qualifying purchases.
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
# Define b and a for your digital filter before this example.
fs = 48_000 # Example sampling frequency in Hz
w, h = signal.freqz(b, a, fs=fs)
fig, ax = plt.subplots()
ax.plot(w, 20 * np.log10(np.maximum(np.abs(h), 1e-12)))
ax.set(
xlabel="Frequency (Hz)",
ylabel="Magnitude (dB)",
title="Digital filter frequency response",
)
ax.grid(True)
plt.show()
freqz returns (w, h): w is the frequency grid and h is the complex response. The example plots the response magnitude in decibels. The 1e-12 floor prevents taking the logarithm of zero; it is a display choice, not a measured lower bound on the filter’s response.
For a linear magnitude plot, replace the plotted expression with np.abs(h) and label the vertical axis “Magnitude” or “Gain.” The SciPy signal-processing tutorial also demonstrates plotting np.abs(H) against frequency in hertz. SciPy signal-processing tutorial
#1 Best Overall
- describes the field of parameter estimation based on time series data.
- provides a summary of principal approaches as well as a “roadmap” to use in the selection of an estimator.
- extends many of the results for real data/real parameters to complex data/complex parameters.
- summarizes as examples many of the important estimators used in practice.
- illustrates how a digital computer can be used to assess performance of an estimator.
Set the frequency range and units
The documented signature is freqz(b, a=1, worN=512, whole=False, plot=None, fs=2*pi, include_nyquist=False). By default, the function samples the one-sided response from zero toward the Nyquist frequency. With the default fs=2*pi, frequencies are in radians per sample; passing your sampling frequency makes them use that frequency’s units, such as hertz.
- Use hertz: pass the actual sampling frequency, such as
fs=48_000. The returned grid then runs from zero towardfs/2by default. - Choose grid density: an integer
worNrequests that many frequency samples. Its default is 512. - Include the Nyquist endpoint: set
include_nyquist=Truewhenwhole=FalseandworNis an integer. In other cases, this option is ignored. - Evaluate specific frequencies: pass an array as
worN; its values are interpreted in the same units asfs. - Calculate a full-circle response: set
whole=Trueto span zero tofs, rather than the default range ending at Nyquist.
These frequency-grid and unit behaviors are documented in the SciPy freqz API reference. When plotting a filter designed with a SciPy design function, keep its sampling-frequency convention consistent with the fs passed to freqz, and label the axis explicitly. The SciPy tutorial discusses sampling-frequency conventions.
Add phase when you need it
The response is complex, so its phase is available from np.angle(h). Unwrap it to avoid jumps at the wrap boundary, then plot it on a separate axis so magnitude and phase remain readable:
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
fig, (ax_mag, ax_phase) = plt.subplots(2, 1, sharex=True)
ax_mag.plot(w, 20 * np.log10(np.maximum(np.abs(h), 1e-12)))
ax_mag.set_ylabel("Magnitude (dB)")
ax_mag.grid(True)
ax_phase.plot(w, np.unwrap(np.angle(h)))
ax_phase.set_xlabel("Frequency (Hz)")
ax_phase.set_ylabel("Phase (rad)")
ax_phase.grid(True)
plt.show()
Use the same frequency grid w for both plots. SciPy’s API example likewise presents unwrapped phase separately. See the freqz API example.
Avoid plotting the real part by mistake
Do not pass matplotlib.pyplot.plot directly as freqz’s plot callback when you want amplitude. SciPy warns that this callback plots the real part of the complex response, not its magnitude. Calculate w, h and plot np.abs(h) explicitly, or provide a callback that plots the absolute value. SciPy documents this callback behavior.
Use the SOS response function for second-order sections
If your filter is represented as second-order sections (SOS), use scipy.signal.freqz_sos to evaluate that representation rather than converting it to one set of polynomial coefficients solely for plotting. SciPy documents a high-order-filter example in which numerical error affects the response calculated from a single transfer-function coefficient representation, and compares it with the SOS calculation. This is a representation and numerical-stability consideration; it does not mean every freqz calculation is inaccurate.
SciPy freqz_sos API reference · SciPy signal-function index
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchQuick Recap
Check before comparing response plots
- Confirm both plots use the same sampling frequency and frequency-axis units.
- Check grid density and whether the Nyquist endpoint is included.
- Distinguish linear magnitude from decibels.
- Read phase on its own scale and note whether it has been unwrapped.
- Check whether each response was computed from polynomial coefficients or SOS sections, especially for a high-order filter.
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




