Command Palette

Search for a command to run...

Hectal
PHASE 14Intermediate ~29 min· topic 1 of 10

Topic 14.1

NumPy Arrays

In one line

A NumPy array (ndarray) is a grid of numbers that all share one type, stored side by side in memory. Because of that, maths on a whole array runs in fast compiled code instead of a slow Python loop.

Think of it like this

An egg tray. Every cup is the same size, the cups sit in neat rows, and you can say "the whole tray" or "the third cup in the second row". A Python list is more like a box of mixed things: an egg, a spoon, a note. It holds anything, but you must open each item to know what it is. A NumPy array is the egg tray: one kind of thing, in a fixed shape.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

NumPy
The package that provides fast arrays and maths on them. Short name: np.
ndarray
NumPy's array type: a grid of same-typed numbers. The name means n-dimensional array.
dtype
The data type shared by all elements of an array, such as float64 or int64.
shape
A tuple with the size of each dimension. A 2-row, 3-column table has shape (2, 3).
ndim
The number of dimensions. A vector has 1, a table has 2.
Dimension
One direction you can step through the data. A row is 1 dimension, a table is 2.
Vector
A one-dimensional array of numbers: a single row, like a list of prices.
Matrix
A two-dimensional array of numbers: rows and columns, like a spreadsheet.
Element-wise
Applying an operation to each element, or to each matching pair of elements.
Overflow
A whole number too big for its type wraps around to a wrong value without an error.

Step by step

01Make an array and look at it

np.array takes a list and builds an ndarray. Print it and the brackets have no commas, one hint that this is not a list. The four facts to check first are dtype, shape, ndim and size.

Here the list holds 30.0, 12.5, 8.0, 99.0. Python floats become float64. Note the shape (4,): a tuple with one number, so the trailing comma is part of the syntax.

main.pywhole filepython
import numpy as np

prices = np.array([30.0, 12.5, 8.0, 99.0])
print(prices.dtype, prices.shape, prices.ndim, prices.size)

02Two dimensions: a list of lists

A list of equal-length lists becomes a 2-D array. The outer list gives the rows. shape is (rows, columns), so (2, 3) is two rows of three. The lists must have the same length. If they do not, NumPy 2 raises an error instead of guessing.

The example passes dtype=np.int64 so the output is the same in the browser and on a desktop. The reason is in the key ideas above: the browser's default integer is 32-bit.

main.pywhole filepython
grid = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int64)
print(grid.shape)    # (2, 3): 2 rows, 3 columns
Two dimensions: a list of listsdiagram
Rendering diagram…

03Ready-made arrays

You rarely type big arrays. np.zeros(3) and np.ones((2, 2)) make arrays filled with 0.0 and 1.0 (the shape goes in as a tuple). np.arange(0, 10, 2) is range that returns an array: start, stop (excluded), step. np.linspace(0, 1, 5) gives 5 evenly spaced numbers with both ends included.

Use arange when you know the step and linspace when you know how many points you want. With float steps, arange can surprise you with rounding, which is a good reason to prefer linspace for floats.

main.pywhole filepython
print(np.zeros(3))
print(np.arange(0, 10, 2))
print(np.linspace(0, 1, 5))

04Maths without a loop

With a list, prices * 2 repeats the list. With an array it multiplies each number. The same goes for +, -, /, ** and comparisons: a > 1.5 gives an array of True and False (called a mask, Topic 14.2).

Two arrays of the same shape combine position by position. Arrays of different shapes can still combine under the broadcasting rules of Topic 14.3.

main.pywhole filepython
a = np.array([1.0, 2.0, 3.0])
b = np.array([10.0, 20.0, 30.0])
print(a + b)    # [11. 22. 33.]
print(a * 2)    # [2. 4. 6.]

05One type for everything

Mixing ints and floats gives floats. Mixing numbers and text turns every element into text, which is almost never what you want, so a U dtype in numeric work is a warning sign.

You can ask for a type with dtype= or convert later with .astype(np.float32). Smaller types save memory (a model with a billion float32 weights needs 4 GB, with float64 8 GB) but hold less precision. np.float32(0.1) + np.float32(0.2) prints 0.3, while Python's float gives 0.30000000000000004.

06Overflow is silent

A Python int grows as large as memory allows. A NumPy integer has a fixed size. Adding two int8 values of 100 gives 200, which does not fit in 8 bits (-128 to 127), so it wraps to -56 and no error appears.

Sums of large counts are where this bites in practice. In the browser the default integer is 32 bits, so a sum above 2,147,483,647 wraps. Use dtype=np.int64 or floats when totals can be big.

main.pywhole filepython
small = np.array([100, 100], dtype=np.int8)
print(small.sum(dtype=np.int8))   # -56

07Why arrays are faster than lists

A list holds *pointers* (addresses that say where each item lives) to Python objects scattered in memory. Each number is a full object with a header. An array holds the raw numbers in one block. A loop in Python must follow each pointer and run Python's internal instructions (bytecode, Topic 13.3) for every element. An array operation is one call into compiled C that streams through the block, often with SIMD instructions (the processor handles several numbers in one step). Topic 13.9 showed this underneath.

The speed-up is large for big arrays and small for tiny ones: calling NumPy has a fixed overhead, so for a three-element list a plain Python loop may actually be faster.

Why arrays are faster than listsdiagram
Rendering diagram…

Try it yourself

  1. 1

    Predict the shape

    Before running, predict the shape of np.array([[1, 2], [3, 4], [5, 6]]), then add print(...shape) and check. Then make the lists different lengths and read the error.

  2. 2

    List versus array

    In the second example change * 2 to + 2 for both the list and the array. The list line raises a TypeError; the array adds 2 to each number. Why can a list not do this?

  3. 3

    Cause an overflow

    Change the int8 example to [100, 27] and [100, 28]. Where does it wrap? Then change the dtype to np.int16 and see it stop wrapping.

Code & diagrams

Create an array and inspect it New tab

Change a number in the first list to an int-only list like [1, 2, 3] and print the dtype: it will say int32 in the browser and int64 on most desktops.

Sign in to run this example in your browser.

Expected output

array: [30.  12.5  8.  99. ]
type: ndarray
dtype: float64
shape: (4,)
ndim: 1
size: 4
[[1 2 3]
 [4 5 6]]
grid shape: (2, 3) ndim: 2
zeros: [0. 0. 0.]
ones: [[1. 1.]
 [1. 1.]]
arange: [0 2 4 6 8]
linspace: [0.   0.25 0.5  0.75 1.  ]
Element-wise maths, and one type for all New tab

Compare the first two lines: the same * means repeat for a list and multiply for an array. For a realistic use, think of a shop's price list: one line, arr * 1.18, adds 18% tax to every price at once.

Sign in to run this example in your browser.

Expected output

list * 2   : [30.0, 12.5, 8.0, 99.0, 30.0, 12.5, 8.0, 99.0]
array * 2  : [ 60.  25.  16. 198.]
array + 1  : [ 31.   13.5   9.  100. ]
array ** 2 : [ 900.    156.25   64.   9801.  ]
a + b: [11. 22. 33.]
a * b: [10. 40. 90.]
a > 1.5: [False  True  True]
mixed dtype: float64
text array: ['1' 'a'] U
int8 small: -56
float32 : 0.3
float64 : 0.30000000000000004
How a list and an array sit in memorydiagram
Rendering diagram…

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

A ragged list of lists

Build an array from rows of different lengths.

main.pywhole filepython
import numpy as np

np.array([[1, 2, 3], [4, 5]])
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 3, in <module>
np.array([[1, 2, 3], [4, 5]])
~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
ValueError: setting an array element with a sequence. The requested array has an inhomogeneous shape after 1 dimensions. The detected shape was (2,) + inhomogeneous part.

Myth vs fact

Myth

A NumPy array is just a faster list.

Fact

It has a fixed type and fixed shape, and its operators mean element-wise maths. It is a different tool: great for numbers, poor for mixed or growing data.

Myth

Integer arrays work like Python ints.

Fact

Python ints are unlimited; NumPy ints have a fixed size and wrap silently on overflow. In the browser the default integer is 32 bits.

Myth

NumPy is always faster.

Fact

For a handful of elements the call overhead makes it slower than plain Python. It wins on large arrays.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    An ndarray is a small header (data pointer, dtype, shape, strides) over a block of bytes. Strides say how many bytes to step to reach the next element along each axis. Many operations (transpose, slicing, reshape) only change the header, which is why they are instant and share memory.

  • ▸

    Memory is size * dtype.itemsize bytes. A million float64 values take 8 MB; as float32 4 MB. Machine-learning code uses float32, float16 and bfloat16 to fit models in memory and speed up arithmetic.

  • ▸

    np.float64 is a subclass of Python float; np.int64 is not a subclass of int. Use .item() or .tolist() to get plain Python numbers when you need them, for example before json.dumps, which cannot serialise arrays or most NumPy scalars (np.int64 and np.float32 fail; np.float64 happens to work because it is a float).

  • ▸

    NumPy 2 treats a bare Python number as "weak": it adopts the array's dtype instead of upgrading it. So float32_array * 3.0 stays float32, and int8_array + 200 raises OverflowError because 200 does not fit in int8. The silent wrap-around happens when two values that already have the dtype are combined (such as sum or array + array). In-place maths also refuses to change type: int_array += 1.5 raises UFuncTypeError, because the float result cannot be stored back into integers without losing the decimals.

  • ▸

    np.empty returns memory that has not been cleared, so it holds leftover garbage until you fill it; use np.zeros unless you will overwrite every element. np.arange with float steps can produce one element more or fewer than you expect because of rounding, so for floats ask for a count with linspace.

Remember this

  1. 1

    NumPy is a package: code written by other people that you install and then import (Topics 6.1 and 6.5). It is not part of the standard library that ships with Python. You write import numpy as np (the short name np is a universal habit) and create an array with np.array([...]). The object you get is an ndarray: an n-dimensional array.

  2. 2

    A dimension is one direction in which you can step through the data. Picture it in words. One dimension is a single row of egg cups: you can only walk left or right (a vector). Two dimensions is the whole tray: you can also walk forward and back, so a cup needs two numbers to find it, its row and its column (a matrix). Three dimensions is a stack of trays: you also choose which tray. More are possible (a batch of colour images has four).

  3. 3

    Every array has a dtype: the one type shared by all its elements, such as float64 (a 64-bit decimal number) or int64. One type means NumPy knows each element takes the same number of bytes, so it can jump straight to any element and run maths in C without asking "what kind of object is this?" a million times. If you mix types, NumPy picks the one type that can hold them all: ints and floats become floats, numbers and text become text.

  4. 4

    Every array also has a shape: a tuple (Topic 4.3) giving the size of each dimension. (4,) is a vector of four numbers. (2, 3) is a table of 2 rows and 3 columns. ndim is the number of dimensions, and size is the total count of elements. Beginners lose most of their NumPy time to shape mistakes, so get used to printing .shape often.

  5. 5

    Maths is element-wise: arr * 2 multiplies every element, and a + b adds matching positions. Compare a list, where [1.0, 2.0] * 2 gives a longer list [1.0, 2.0, 1.0, 2.0]. In NumPy the operators mean arithmetic. You write one expression for a million numbers and no loop. This is called vectorisation (Topic 14.3).

  6. 6

    Arrays have a fixed size and a fixed type. That is the price of speed. You cannot append to an array in place the way you do with a list (Topic 4.1). Adding an item creates a new array. When you need a growing collection, build a list and convert once at the end with np.array(...).

  7. 7

    Types have limits you should know. Integer types wrap around when they overflow, like a car's odometer rolling from 99999 back to 00000 (an int8 holds -128 to 127). float32 holds about 7 decimal digits, float64 about 15 or 16. **In the browser, NumPy runs as a 32-bit WebAssembly build, so a default integer array is int32 there and int64 on a typical desktop.** Sums above about 2.1 billion can overflow silently in the browser. When integer size matters, say so: dtype=np.int64.

  8. 8

    Why does AI care? A tensor (PyTorch's name for an n-dimensional array), a dataset in scikit-learn and an embedding (a list of numbers a model uses to describe a piece of text or an image, Topic 14.5) are all arrays shaped like these. If you can read (32, 128) as 32 examples with 128 numbers each, you can read most machine-learning code.

Explain it without notes

01

What is the difference between a Python list and a NumPy array?

02

What do shape, ndim and dtype tell you? Give the values for np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]).

03

Why is a NumPy array faster than a loop over a list for maths?

04

What happens when you put ints, floats and strings in one array? Why does that matter?

05

A learner's integer total comes out negative in the browser but correct on their laptop, with no error. What happened and how do you prevent it?

Practice

01

Create an array of the numbers 0, 5, 10, ..., 50 using np.arange, and print its shape and size.

02

Make a (3, 2) array of ones, multiply it by 7, and print it and its shape.

03

Given celsius = np.array([0.0, 37.0, 100.0]), compute the Fahrenheit values with one expression (c * 9 / 5 + 32).

Trade-offs

  • ↔

    Speed and compact memory versus flexibility. One fixed type and shape is what makes arrays fast, and also why you cannot mix types or append cheaply.

  • ↔

    Smaller dtypes versus precision. float32 halves memory and speeds up many operations, but holds fewer digits; int8 overflows quickly.

  • ↔

    Vectorised code versus readability for beginners. One-line array maths is short, but needs shapes in your head; a loop is longer but easier to step through.

Done when you can

  • Done when you can create arrays with np.array, zeros, ones, arange and linspace.

  • Done when you can read shape, ndim, size and dtype and say what each means.

  • Done when you can explain why arr * 2 and lst * 2 differ.

  • Done when you can say why mixed inputs change the dtype, and spot an accidental text array.

  • Done when you can explain integer overflow and why the browser's default integer is 32-bit.