NumPy (Numerical Python) is an open-source Python library that provides an efficient multi-dimensional array type (`ndarray`) along with a large collection of mathematical functions that operate on those arrays. It serves as a foundational numerical computing layer for much of the scientific Python ecosystem, including SciPy, pandas, Matplotlib, and scikit-learn.
In practice
In embedded-adjacent work, NumPy is rarely used on the target device itself; it runs on a host PC or development workstation. Common uses include processing data captured from embedded systems (ADC samples, sensor logs, bus captures), rapid prototyping of DSP algorithms (filters, FFTs, resampling) before porting the logic to C/C++, and generating test vectors or coefficient tables to be compiled into firmware. Blog posts such as "Polyphase Filters and Filterbanks" and "Generating pink noise" illustrate this pattern: work out the algorithm in NumPy first, then translate to fixed-point C for the target.
NumPy's vectorized operations replace explicit Python loops with calls into optimized C and Fortran routines, which typically yields significant speedup over pure Python for array-heavy work -- often one to two orders of magnitude, though the actual gain is highly workload-dependent and can vary based on operation type and memory bandwidth. The post "Python number crunching faster? Part I" explores this performance gap and strategies for pushing it further. For embedded developers who are more comfortable in C, the mental model is similar to operating on whole arrays at once rather than element by element, much like SIMD intrinsics but with far more ergonomic syntax.
A common workflow is to use NumPy (often together with SciPy and Matplotlib) to design and validate a filter or algorithm, export the resulting coefficients as a C header using `numpy.savetxt` or a simple format-string loop, and then integrate those coefficients into the firmware build. The post "Approximating the area of a chirp by fitting a polynomial" demonstrates this kind of host-side numerical work feeding directly into embedded use. Similarly, polynomial or filter coefficients computed with `numpy.polyfit` or `scipy.signal` can be pasted or auto-generated into a `const float` array in firmware.
Full NumPy is not designed for microcontrollers and does not run on most of them in practice, as it requires CPython, native C extensions, an OS, and significant RAM. MicroPython and CircuitPython provide a limited subset called `ulab` (modeled on NumPy) for capable MCUs such as the Raspberry Pi Pico (RP2040) or certain STM32-based boards, though support varies by board, firmware build, and available memory; the API coverage and performance characteristics differ significantly from desktop NumPy. For serious numerical work in the design loop, the host-side full NumPy installation is the standard tool.
Discussed on DSPRelated
Frequently asked
Can NumPy run on a microcontroller?
Full NumPy cannot in practice; it depends on CPython and native C extensions that require an OS and significant RAM. The `ulab` library offers a NumPy-compatible subset for MicroPython on capable MCUs (e.g., RP2040, STM32F4-class parts with sufficient flash and RAM), but support varies by board and firmware build, coverage is partial, and performance is far below desktop NumPy. For most embedded targets, NumPy stays on the host.
How does NumPy fit into a typical DSP prototyping workflow for embedded systems?
A common pattern is to prototype and validate the algorithm entirely in NumPy/SciPy on the host, verify correctness against reference data, then hand-translate the core math to C or use code generation tools. NumPy's `ndarray` operations map relatively cleanly to C array loops, and coefficient arrays can be exported directly as C header files.
What is the relationship between NumPy and SciPy?
NumPy provides the core array type and basic math operations, including subpackages for linear algebra,
FFT, random number generation, and polynomial fitting. SciPy builds on top of NumPy and adds higher-level algorithms: signal processing (`scipy.signal`), statistics, optimization,
interpolation, and more. For embedded DSP work, `scipy.signal` is especially useful for filter design, while NumPy handles the underlying array math.
Why is NumPy so much faster than plain Python for numerical work?
Python loops carry per-iteration interpreter overhead and dynamic type resolution. NumPy moves the loop into compiled C (and sometimes Fortran) code operating on contiguous, typed memory buffers. The result is that element-wise operations on large arrays typically run much faster than pure Python, though the exact speedup is workload-dependent. The blog post 'Python number crunching faster? Part I' examines this speedup in concrete terms.
How do I get filter coefficients from NumPy/SciPy into my firmware?
Design the filter with `scipy.signal` (e.g., `butter`, `firwin`), which returns coefficient arrays as NumPy `ndarray` objects. Then format them for C using a Python snippet or `numpy.savetxt`. For example, `', '.join(f'{c:.10f}f' for c in b)` produces a C initializer list suitable for a `const float` array in a header file.
Differentiators vs similar concepts
NumPy and SciPy are complementary, not interchangeable. NumPy supplies the foundational `ndarray` type, basic linear algebra,
FFT, and polynomial routines (organized into subpackages such as `numpy.linalg`, `numpy.fft`, and `numpy.random`). SciPy extends this with domain-specific algorithms including signal processing (`scipy.signal`), optimization, and statistics. In practice, embedded DSP prototyping uses both together. NumPy should not be confused with `ulab`, which is a partial, MicroPython-targeted reimplementation intended for microcontrollers; `ulab` covers a subset of NumPy's API, has different performance characteristics, and varies in support across boards and firmware builds.