When working with numerical data in Python, the only credible choice is NumPy’s ndarray. This object generalises the humble list into a homogeneous, fast multidimensional container that backs nearly even scientific-computing stack. In practice, you will reach for just two ranks: one-dimensional arrays (vectors) and two-dimensional arrays (matrices). Each rank adds an axis - so a vector owns a single axis, a matrix owns two, and the pattern extends naturally to tensors of higher order.
Every array carries a concise self-description:
size tells you the total count of stored elements - nothing more, nothing less.shape is a tuple whose i-th entry gives the length of the i-th axis; for a classical $m\times n$ matrix that is (m, n).ndim returns the rank (the number of axes), a scalar measure of structural complexity.dtype declares the underlying C-level scalar type (int32, float64, and so on). Choosing the right dtype is not a detail: it controls memory footprint, numerical range and performance.Creating arrays is trivial but worth doing correctly. The most explicit route is wrapping a Python sequence:
numpy.array([1, 2, 3], dtype=numpy.float64)
The optional dtype gives you deterministic, portable storage. Cloning an existing array is equally direct: y = numpy.array(x) produces a copy that can diverge safely from its parent.
For initialisation patterns that occur repeatedly in real code, favour NumPy’s factory functions - they are clearer and faster than hand-rolled loops:
| Constructor | Semantics (example) |
|---|---|
numpy.zeros(shape, dtype) |
All-zero array, e.g. numpy.zeros((2, 3)) |
numpy.ones(shape) |
All-one array, e.g. numpy.ones(5) |
numpy.arange([start,] stop[, step]) |
Half-open range with optional step, e.g. numpy.arange(0, 6, 2) → [0, 2, 4] |
numpy.eye(n) |
Identity matrix of size n |
numpy.linspace(start, stop, num) |
num evenly spaced samples between the endpoints, inclusive |
Once you have data, element-wise arithmetic “just works”. The usual infix operators (+, -, *, /) broadcast across matching shapes, yielding new arrays:
x + y # pairwise sum
x * y # pairwise product
If you intend to mutuate in place - typically for large arrays where allocations hurt - append an equals sign (x += y, x *= y). This idiom is both faster and memory-frugal.
For genuine matrix multiplication, abandon the element-wise *. The canonical interface is numpy.dot(A, B); in modern Python you may prefer the infix @ operator for readability.
C = A @ B # same as numpy.dot(A, B)
Here, respect the algebraic contract: the column count of A must equal the row count of B. NumPy will raise immediately if the dimensions are incompatible - an early failure that saves hours of silent corruption.
Arrays are not frozen in their original shapes. With reshape you can reinterpret the same buffer under a new geometry so long as the total element count is preserved.