Free lesson · Python Basic to Advance
NumPy Arrays
NumPy Arrays
What NumPy Is
NumPy is the foundational library for numerical computing in Python. Its central data structure is the ndarray - a multidimensional array of homogeneous values backed by a contiguous block of memory. Operations on ndarrays are vectorized: a single function call applies element-wise across the whole array, with the inner loop running in C rather than Python. The result is performance that approaches native compiled code while keeping the convenience of Python.
import numpy as np
# Create an array
a = np.array([1, 2, 3, 4, 5])
print(a) # [1 2 3 4 5]
print(a.dtype) # int64
print(a.shape) # (5,)
# Vectorized operation - no explicit loop
b = a * 2
print(b) # [ 2 4 6 8 10]
# Operate element-wise between arrays
c = a + b
print(c) # [ 3 6 9 12 15]
This code creates a NumPy array and performs vectorized operations. Multiplying by two scales every element. Adding two arrays adds them element-wise. No explicit for loop - the work happens in a fast C loop under the hood. This vectorization is what makes NumPy fast for numerical work.
Install with pip install numpy. Convention: import numpy as np. The alias is universal - nearly all NumPy code uses it.
Why NumPy
NumPy earns its place because:
- Speed - vectorized operations are 10x to 100x faster than equivalent Python loops
- Memory efficiency - arrays are contiguous and homogeneous; far less overhead than lists
- Broadcast semantics - operations between arrays of different shapes follow consistent rules (Topic 27.2)
- Ecosystem - pandas, scikit-learn, scipy, PyTorch, TensorFlow all build on NumPy
- Mathematical surface - linear algebra, FFTs, random numbers, statistics, all in one library
A simple comparison:
import numpy as np
# Python list - slow
py_list = list(range(1_000_000))
result = [x * 2 for x in py_list] # tens of milliseconds
# NumPy array - fast
np_array = np.arange(1_000_000)
result = np_array * 2 # ~10x faster, less memory
This code contrasts a list and an array for the same operation. The list comprehension runs in Python, looping element by element. The array operation runs vectorized in C. For a million elements, the speed difference is roughly tenfold. For larger data or more complex operations, the gap widens further.
For numerical work, that gap is the difference between code that runs in seconds and code that runs in hours.
Creating Arrays
Several ways to construct arrays:
import numpy as np
# From a Python list
a = np.array([1, 2, 3])
# Multidimensional from nested lists
m = np.array([[1, 2, 3], [4, 5, 6]])
print(m.shape) # (2, 3)
# Zeros, ones, empty
zeros = np.zeros((3, 4))
ones = np.ones((2, 2))
empty = np.empty(5) # uninitialized; values are arbitrary
# Range-like
r = np.arange(10) # 0..9
r2 = np.arange(0, 1, 0.1) # 0, 0.1, 0.2, ..., 0.9
linspace = np.linspace(0, 1, 11) # 11 evenly spaced points from 0 to 1
# Identity matrix
i = np.eye(3)
# Filled with a value
full = np.full((2, 3), 7)
This code shows array constructors. Array converts a list or nested list. Zeros, ones, and full create arrays of a fixed value. Arange is like Python's range but returns an array. Linspace is preferable for evenly spaced floating-point sequences - it specifies the count rather than the step, avoiding float-precision surprises. Eye creates an identity matrix.
The most commonly used: np.array from existing data, np.zeros and np.ones for placeholders, np.arange and np.linspace for sequences.
Data Types
Every ndarray has a single dtype - every element has the same type:
import numpy as np
# Default for integer literals
a = np.array([1, 2, 3])
print(a.dtype) # int64 (on most systems)
# Default for floats
b = np.array([1.0, 2.0])
print(b.dtype) # float64
# Explicit dtype
c = np.array([1, 2, 3], dtype=np.float32)
print(c.dtype) # float32
d = np.array([1, 2, 3], dtype=np.int8) # tiny integers
print(d.dtype) # int8
# Common dtypes
# int8, int16, int32, int64
# uint8, uint16, uint32, uint64
# float16, float32, float64
# complex64, complex128
# bool
This code shows dtypes. Each array stores elements of one type. Default integer is int64 on most systems; default float is float64. Smaller dtypes save memory - useful for large arrays where the range fits. Choosing the right dtype affects memory usage and sometimes speed.
For large arrays, using float32 instead of float64 halves memory. For categorical-like integer data, int8 or int16 can be much smaller than the default. The trade-off is range and precision.
Shape, Size, and Dimensions
Every array has a shape that describes its dimensions:
import numpy as np
# 1-D
a = np.array([1, 2, 3, 4])
print(a.shape) # (4,)
print(a.ndim) # 1
print(a.size) # 4
# 2-D
m = np.array([[1, 2, 3], [4, 5, 6]])
print(m.shape) # (2, 3) - 2 rows, 3 columns
print(m.ndim) # 2
print(m.size) # 6
# 3-D
t = np.zeros((2, 3, 4))
print(t.shape) # (2, 3, 4)
print(t.ndim) # 3
print(t.size) # 24
This code shows array shapes. The shape is a tuple of dimension sizes. Ndim is the number of dimensions. Size is the total element count - the product of the shape's components. Two-D arrays are the most common in data work - they represent tables, matrices, and images. Three-D and higher appear in scientific computing and machine learning.
The shape (rows, columns) convention matches matrix math. For images, (height, width, channels) is typical.
Reshaping
Arrays can be reshaped without copying data, as long as the total size matches:
import numpy as np
a = np.arange(12)
print(a.shape) # (12,)
# Reshape to 2-D
m = a.reshape(3, 4)
print(m.shape) # (3, 4)
print(m)
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
# -1 means "infer this dimension"
m2 = a.reshape(2, -1) # 2 rows, columns inferred
print(m2.shape) # (2, 6)
# Flatten back to 1-D
flat = m.flatten()
print(flat.shape) # (12,)
# Transpose
t = m.T
print(t.shape) # (4, 3)
This code reshapes arrays. Reshape returns a view of the same data with a new shape - fast, no copy. Minus one infers a dimension from the total size. Flatten returns a 1-D copy. The dot T attribute transposes - rows become columns. Reshape and transpose are the most common shape operations in data work.
Reshape doesn't copy data (unless the new shape isn't compatible with the memory layout). For huge arrays, this matters.
Indexing - Basics
NumPy supports rich indexing schemes:
import numpy as np
a = np.array([10, 20, 30, 40, 50])
# Single element
print(a[0]) # 10
print(a[-1]) # 50
# Slicing - returns a view, not a copy
print(a[1:4]) # [20 30 40]
print(a[::2]) # [10 30 50] - every other element
# Modifying a slice modifies the original
a[1:4] = 0
print(a) # [10 0 0 0 50]
This code uses basic indexing. Integer indices select single elements. Slices return views - modifying the slice modifies the underlying array. This differs from Python lists, where slicing copies. The view behavior is faster but easier to misuse: an unintended modification to a slice changes the original.
The slice-as-view distinction matters. For independent data, use .copy().
Indexing - Multidimensional
For 2-D and higher arrays:
import numpy as np
m = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
# Single element - (row, col)
print(m[0, 0]) # 1
print(m[1, 2]) # 6
# Row
print(m[0]) # [1 2 3]
print(m[0, :]) # same - [1 2 3]
# Column
print(m[:, 0]) # [1 4 7]
print(m[:, 1]) # [2 5 8]
# Sub-matrix
print(m[0:2, 1:3]) # [[2 3] [5 6]]
This code shows 2-D indexing. Commas separate dimensions inside the brackets. A single integer or slice per dimension. Colons select all elements in a dimension. The result of a 2-D indexing operation may be a scalar, a 1-D slice, or a 2-D submatrix depending on what's selected.
The comma-separated form (m[0, 1]) is preferred over chained brackets (m[0][1]) - it's faster and matches the mathematical notation.
Boolean Indexing
Filter arrays using boolean masks:
import numpy as np
a = np.array([1, 2, 3, 4, 5, 6])
# Comparison produces a boolean array
mask = a > 3
print(mask) # [False False False True True True]
# Boolean indexing - select where mask is True
print(a[mask]) # [4 5 6]
# Combine conditions
mask = (a > 2) & (a < 5)
print(a[mask]) # [3 4]
# Assignment via boolean mask
a[a > 3] = 0
print(a) # [1 2 3 0 0 0]
This code uses boolean indexing. Comparison operators produce boolean arrays of the same shape. Using a boolean mask as an index selects only the True positions. Combining conditions uses ampersand and pipe - not the Python and or or, which don't work element-wise. Boolean masks are one of NumPy's most useful features for filtering numerical data.
For element-wise logical operations, use &, |, and ~ - not and, or, not. The latter are short-circuiting Python operators and don't work on arrays.
Fancy Indexing
Select arbitrary elements by index array:
import numpy as np
a = np.array([10, 20, 30, 40, 50])
# Pick specific indices
indices = np.array([0, 2, 4])
print(a[indices]) # [10 30 50]
# Indices can repeat
print(a[[0, 0, 1, 1]]) # [10 10 20 20]
# For 2-D arrays
m = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
rows = np.array([0, 2])
cols = np.array([1, 2])
print(m[rows, cols]) # [2 9] - m[0,1] and m[2,2]
This code uses fancy indexing - an array of indices instead of slices. Useful for picking specific elements without writing a loop. For 2-D arrays, providing two index arrays selects pairs of coordinates. Fancy indexing returns a copy, not a view - unlike slices.
Useful Array Methods
A few methods that come up often:
import numpy as np
a = np.array([3, 1, 4, 1, 5, 9, 2, 6])
# Aggregations
print(a.sum()) # 31
print(a.mean()) # 3.875
print(a.min(), a.max()) # 1 9
print(a.std()) # standard deviation
# Sorted - returns a copy
print(np.sort(a)) # [1 1 2 3 4 5 6 9]
# Argsort - indices that would sort the array
print(np.argsort(a)) # [1 3 6 0 2 4 7 5]
# Unique values
print(np.unique(a)) # [1 2 3 4 5 6 9]
# Cumulative
print(np.cumsum(a)) # [ 3 4 8 9 14 23 25 31]
This code shows common methods. Sum, mean, min, max, std are the basic aggregations. Sort returns a sorted copy; argsort returns indices. Unique gives the unique values. Cumsum computes the running total. These methods work on 1-D arrays directly, and on multidimensional arrays with an axis argument - Topic 27.2 covers this.
For multidimensional arrays, most aggregations accept an axis argument to operate along one direction.
Copies vs Views
A common source of confusion:
import numpy as np
a = np.array([1, 2, 3, 4, 5])
# Slice - view, shares data
b = a[1:4]
b[0] = 99
print(a) # [ 1 99 3 4 5] - original changed!
# Fancy indexing - copy
c = a[[1, 2, 3]]
c[0] = 0
print(a) # original unchanged
# Explicit copy
d = a[1:4].copy()
d[0] = 0
print(a) # unchanged
This code shows the copy versus view distinction. Slices return views - modifying them modifies the original. Fancy indexing returns copies. When in doubt, call dot copy explicitly. For functions that should not mutate their inputs, document the contract clearly or call copy at the boundary.
The rule of thumb: basic slicing is a view; advanced (boolean or fancy) indexing is a copy. When in doubt, use .copy().
Memory Layout
NumPy arrays are stored as contiguous blocks of memory. Two layouts:
- C-order (row-major) - default; rows are contiguous
- Fortran-order (column-major) - columns are contiguous
import numpy as np
m = np.array([[1, 2, 3], [4, 5, 6]])
print(m.flags["C_CONTIGUOUS"]) # True
print(m.flags["F_CONTIGUOUS"]) # False
# Force Fortran order
mf = np.asfortranarray(m)
print(mf.flags["F_CONTIGUOUS"]) # True
This code inspects memory layout. C-order matches C and most other languages - rows are stored sequentially. Fortran-order - used by some linear algebra libraries - stores columns sequentially. For most application code, the default is correct. Some scientific libraries are faster with one layout or the other.
For application code, defaults are usually right. For performance-critical interop with libraries (BLAS, LAPACK), layout sometimes matters.
Performance
NumPy's performance comes from:
- Vectorized C loops instead of Python loops
- SIMD instructions on supported hardware
- Cache-friendly contiguous memory
- Reduced Python object overhead
Typical speedups over pure Python:
- Element-wise arithmetic: 10x–100x faster
- Aggregations (sum, mean): 50x–200x faster
- Matrix multiplication: 1000x+ faster (uses BLAS)
For numerical code, NumPy is usually the right starting point. For even better performance on specific workloads, libraries like numba, cython, or cupy (GPU) extend the same patterns.
Interview Relevance
Interview: NumPy basics are essential Python knowledge for any numerical or data role:
- The
ndarrayas a typed, multi-dimensional, contiguous container- Vectorized operations vs Python loops
- Array creation:
np.array,np.arange,np.linspace,np.zeros/onesdtypeand how to choose it- Shape,
reshape,Ttranspose- Indexing: basic, slicing (returns views), boolean masks, fancy indexing
- Element-wise vs whole-array operations
- Copies vs views - the slice-modifies-original surprise
A common interview moment: a candidate writes a Python
forloop over an array. The right answer is the vectorized form.Another moment: explaining why
a[a > 3] = 0works. The answer involves boolean masks as indices.Module 30 covers Python interview preparation.
Common Pitfalls
Warning: Mixing
and/orwith arrays. Use&,|,~for element-wise logic. Wrap conditions in parentheses - operator precedence is different from Python's.Modifying a slice and surprising the original. Slices are views. Use
.copy()for independent data.Treating an ndarray like a list.
len(a)returns the first dimension size, not the total element count. Use.size.Mixed dtypes. Arrays are homogeneous. Mixing
intandstrupcasts toobjectand loses NumPy's speed.Forgetting axis arguments.
a.sum()for a 2-D array sums everything. Often you wanta.sum(axis=0)ora.sum(axis=1).Ignoring memory usage.
float64for a billion-element array is 8 GB. Choose dtypes appropriate to the data.Building arrays incrementally with
np.append. Each append copies the entire array. Build a list and convert once at the end.
Try It
Exercise: Practice with NumPy:
- Create a 1-D array of 1,000,000 integers. Double each element with both a Python loop and a NumPy operation. Compare timings.
- Build a 4×4 matrix. Practice slicing - rows, columns, sub-matrices.
- Use boolean indexing to find all elements greater than the mean of an array.
- Reshape a 12-element array into a 3×4 matrix, then transpose to 4×3.
- Demonstrate the slice-as-view behavior: modify a slice and observe the original changing. Then use
.copy()to break the link.- Compare memory usage for the same data stored as
int8vsint64. Usea.nbytes.