Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 21 – Python everywhere: data, web, AI and next steps

NumPy first steps: arrays and vectorisation

Create and index NumPy arrays, replace Python loops with vectorised operations and broadcasting, and catch the integer overflow that Python ints never have.

  • Intermediate
  • 25 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7 and numpy 2.5.3
  • By MySmartCoPilot
Free: 25 minutes

What you will learn

  • Create NumPy arrays and index them by row, column, slice and mask
  • Replace Python loops with vectorised operations and broadcasting
  • Predict which edits of a slice or a selection reach the original array
  • Predict when fixed-size integer types overflow, and prevent it
  • Decide when to move on to pandas and the Data Analysis with Python track

Before you start

What you’ll learn · Before you start

What you will learn

  • Create NumPy arrays and index them by row, column, slice and mask
  • Replace Python loops with vectorised operations and broadcasting
  • Predict which edits of a slice or a selection reach the original array
  • Predict when fixed-size integer types overflow, and prevent it
  • Decide when to move on to pandas and the Data Analysis with Python track

Before you start

On this page

A Python loop that totals a million order lines runs its body a million times; one NumPy expression gets the same exact total more than ten times faster on the computer that recorded this lesson’s runs. NumPy is the package most of scientific and data Python is built on, and its centre is one type, the NumPy's array type: a grid of values that all share one data type, stored side by side in memory, with a size fixed when it is made.: a grid of numbers of one type that NumPy works on in compiled code instead of in Python.

You will read and reduce a small table of marks, measure when that speed-up appears and when it does not, step through the rule that lets arrays of different shapes work together, see which edits of a slice reach the original array, and catch the integer overflow that Python’s own integers never have. The last section says when a table belongs in pandas instead.

An array is a grid of one dtype, read one axis at a time

Five students sat four subjects, so a class has 20 marks. np.array() turns the nested lists into one array with five rows and four columns, and the program then reads it in five ways:

A marks table as one array, read five ways Python · marks_table.py
"""Marks of five students in four subjects, held in one NumPy array and read by row, column, element, slice and mask."""
import numpy as np

subjects = ["maths", "physics", "chemistry", "english"]
students = ["Asha", "Ben", "Chen", "Divya", "Emre"]
marks = np.array(
    [
        [78, 64, 81, 70],
        [55, 72, 49, 88],
        [91, 85, 77, 62],
        [66, 58, 93, 75],
        [42, 90, 68, 81],
    ]
)

print("shape:", marks.shape, "| ndim:", marks.ndim, "| size:", marks.size, "| dtype:", marks.dtype)
print("Chen, a row:          ", marks[2])
print("physics, a column:    ", marks[:, 1])
print("Divya in chemistry:   ", marks[3, 2])
print("first two students, last two subjects:")
print(marks[:2, 2:])

below_50 = marks < 50  # a boolean array of the same shape
print("marks < 50, a boolean array of shape", below_50.shape)
print(below_50)
print("marks[marks < 50]:", marks[below_50])

Output

shape: (5, 4) | ndim: 2 | size: 20 | dtype: int64
Chen, a row:           [91 85 77 62]
physics, a column:     [64 72 85 58 90]
Divya in chemistry:    93
first two students, last two subjects:
[[81 70]
 [49 88]]
marks < 50, a boolean array of shape (5, 4)
[[False False False False]
 [False False  True False]
 [False False False False]
 [False False False False]
 [ True False False False]]
marks[marks < 50]: [49 42]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 marks_table.py

Version note

The examples ran with NumPy 2.5.3 on CPython 3.14.8 and show their recorded output. NumPy is a third-party package, and the in-browser runner of this site offers the standard library only, so the NumPy examples have no Run button. To run them yourself, install NumPy in a virtual environment: python -m pip install numpy==2.5.3. The program in this lesson that needs only the standard library, and the exercise at the end, run in your browser.

Four attributes describe every array. shape is the size along each dimension, (5, 4) here. ndim is the number of dimensions, and NumPy calls each one an One dimension of a NumPy array, counted from 0. In a table of shape (5, 4), axis 0 runs down the 5 rows and axis 1 across the 4 columns.: axis 0 runs down the rows and axis 1 across the columns. size is the number of elements, 20, and dtype is the one type they all share, the array’s The one data type that every value of a NumPy array has, such as int64, int32 or float64. It fixes how many bytes each value takes and which values fit.. NumPy chose int64, its default integer on this 64-bit computer, because every value was a Python int. Unlike a list, an array holds values of that one dtype only, and its size is fixed when it is created.

Indexing takes one position or slice per axis, separated by commas. Step through the five expressions the program printed, and watch which rows each one reads and what shape comes back:

Interactive · step by step Five ways to read the marks table Step through five expressions on marks. The rows in focus are the ones each expression reads; the right column shows what comes back and its shape.
Five ways to read the marks table, step 6 of 6. Showing Asha (row 0), Ben (row 1), Chen (row 2), Divya (row 3), Emre (row 4), Expression (marks[marks < 50]), What comes back (49 42), Its shape ((2,)). In focus: Ben (row 1), Emre (row 4).Asha (row 0)78 64 81 70Ben (row 1)55 72 49 88Chen (row 2)91 85 77 62Divya (row 3)66 58 93 75Emre (row 4)42 90 68 81Expressionmarks[marks < 50]What comes back49 42Its shape(2,)

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The whole array. marks holds 20 marks in one array. Axis 0 runs down the five students, axis 1 across maths, physics, chemistry and english, so its shape is (5, 4), and every value is an int64.
  2. marks[2]: a row. One position picks along axis 0, the rows: marks[2] is the third row, Chen's marks. One axis is left, the four subjects, so the shape is (4,).
  3. marks[:, 1]: a column. The colon means every row, and 1 picks the second position on axis 1, physics. One value comes from each student, top to bottom: shape (5,).
  4. marks[3, 2]: one value. One position on each axis picks a single mark: row 3, Divya, and position 2 on axis 1, chemistry. No axis is left, so the result is one number, 93.
  5. marks[:2, 2:]: a block. A slice on each axis keeps both axes: rows 0 and 1, Asha and Ben, and positions 2 and 3, chemistry and english. The result is a smaller table of shape (2, 2).
  6. marks[marks < 50]: a mask. marks < 50 is a boolean array of shape (5, 4), True only at Ben's 49 in chemistry and Emre's 42 in maths. Used as an index, it picks those marks, in row order.

A single position removes its axis from the result: marks[2] is a row of shape (4,), and marks[3, 2], with a position on both axes, is one number. A slice keeps its axis, so marks[:2, 2:] is still a table. A comparison such as marks < 50 gives a An array of True and False values, such as marks < 50, with the shape of the array it was made from. Used as an index, it selects the values where it is True. of the same shape, and using it as an index selects the marks where it is True, as a new array.

Checkpoint

Check: Shape, dtype and indexing by axis

  1. Question 1 of 2 The rows of marks are the five students and its columns are maths, physics, chemistry and english. Which expression gives the english marks of all five students?

    Choose one answer.

    A hint

    Which axis holds the subjects, and what does a colon mean on the other one?

    Show the answer: Question 1 of 2

    Answer: marks[:, 3]

    Indexing takes one position or slice per axis. Axis 1 holds the subjects, so the english column is position 3 of axis 1 with every row kept: marks[:, 3], shape (5,).

  2. Question 2 of 2 What is the shape of marks[1:4, :2], where marks has shape (5, 4)?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: (3, 2)

    Each slice keeps its axis and counts what it selects: 1:4 selects 3 rows and :2 selects 2 columns, so the result is a smaller table of shape (3, 2).

A reduction removes the axis you name

The mean of each subject and the mean of each student come from the same method; only its axis argument differs. Methods such as sum(), mean(), max() and argmax() reduce an array: without an axis they use every value, and with one they work along that axis only:

The marks table reduced three ways Python · marks_reduce.py
"""The marks table reduced: over the whole array, along axis 0 (one result per subject) and along axis 1 (per student)."""
import numpy as np

subjects = ["maths", "physics", "chemistry", "english"]
students = ["Asha", "Ben", "Chen", "Divya", "Emre"]
marks = np.array(
    [
        [78, 64, 81, 70],
        [55, 72, 49, 88],
        [91, 85, 77, 62],
        [66, 58, 93, 75],
        [42, 90, 68, 81],
    ]
)

print("sum of all 20 marks:", marks.sum(), "| their mean:", marks.mean())
print("mean per subject:", marks.mean(axis=0), "shape", marks.mean(axis=0).shape)
print("mean per student:", marks.mean(axis=1), "shape", marks.mean(axis=1).shape)

best = marks.argmax(axis=1)  # the position of the largest mark in each row
print("position of each student's best mark:", best)
print("best subject of each student:", [subjects[i] for i in best])

below_50 = marks < 50
print("how many marks are below 50:", below_50.sum())  # True counts as 1
print("any mark below 50, per student:", below_50.any(axis=1))
print("students with a mark below 50:", [students[i] for i in np.flatnonzero(below_50.any(axis=1))])

Output

sum of all 20 marks: 1445 | their mean: 72.25
mean per subject: [66.4 73.8 73.6 75.2] shape (4,)
mean per student: [73.25 66.   78.75 73.   70.25] shape (5,)
position of each student's best mark: [2 3 0 2 1]
best subject of each student: ['chemistry', 'english', 'maths', 'chemistry', 'physics']
how many marks are below 50: 2
any mark below 50, per student: [False  True False False  True]
students with a mark below 50: ['Ben', 'Emre']

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 marks_reduce.py

axis=0 collapses the rows and gives one result per column, the mean of each subject; axis=1 collapses the columns and gives one result per row, the mean of each student. The printed shapes show the rule: the axis you pass is the one that disappears, so (5, 4) reduced along axis 0 has shape (4,). argmax(axis=1) follows the same rule but returns positions, which the program turns into subject names. And because True counts as 1, the sum() of a mask counts its True values, while any(axis=1) asks the question once per student. Step through the reductions and watch the shape on the right lose the axis each call names:

Interactive · step by step Which axis a reduction removes Step through the reductions of marks_reduce.py. Watch the shape on the right lose the axis each call names.
Which axis a reduction removes, step 5 of 5. Showing Asha (78 64 81 70: none below 50), Ben (55 72 49 88: 49 is below 50), Chen (91 85 77 62: none below 50), Divya (66 58 93 75: none below 50), Emre (42 90 68 81: 42 is below 50), The call ((marks < 50).any(axis=1)), What comes back (False True False False True), Its shape ((5,)). In focus: Ben, Emre.Asha78 64 81 70: none below 50Ben55 72 49 88: 49 is below 50Chen91 85 77 62: none below 50Divya66 58 93 75: none below 50Emre42 90 68 81: 42 is below 50The call(marks < 50).any(axis=1)What comes backFalse True False False TrueIts shape(5,)

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. No axis named. Without an axis, a reduction uses every value of the array: marks.mean() adds all 20 marks, 1,445, and divides by 20. One number comes back, 72.25.
  2. axis=0 removes the rows. mean(axis=0) collapses axis 0: for each subject, the five marks down the rows become one mean. Axis 0 disappears from the shape, so (5, 4) becomes (4,).
  3. axis=1 removes the columns. mean(axis=1) collapses axis 1: each student's four marks become one mean, shown beside each row. Axis 1 disappears, so the shape is (5,).
  4. argmax names a position. argmax(axis=1) gives, for each row, the position of its largest mark: 2, 3, 0, 2 and 1. The program turns those positions into subject names.
  5. Counting with a mask. True counts as 1, so (marks < 50).sum() counts the marks below 50: 2. With axis=1, any() asks the question once per student, and only the rows of Ben and Emre answer True.

Key idea

The axis you pass to a reduction is the axis that disappears: a (5, 4) table gives (4,) along axis 0, (5,) along axis 1, and a single value when no axis is named.

Checkpoint

Check: Reducing along an axis

  1. Question 1 of 2 What is the shape of marks.max(axis=1), and what does it hold?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: (5,): the best mark of each student

    The axis you pass disappears. marks has shape (5, 4); reducing along axis 1, the subjects, leaves axis 0, the five students, so the result has shape (5,): each student's highest mark.

  2. Question 2 of 2 How many marks of the table does (marks > 85).sum() count?

    Type a number.

    A hint

    Look for marks strictly above 85 in the five rows of the figure.

    Show the answer: Question 2 of 2

    Answer: 4 marks

    marks > 85 is True for Ben's 88, Chen's 91, Divya's 93 and Emre's 90, and True counts as 1 in a sum, so the result is 4. Chen's 85 in physics is not greater than 85.

One expression runs the whole loop in compiled code

The next program computes the value of a million order lines twice, once with a Python loop over plain integers and once with the single expression (price_paise * quantity).sum():

A million order lines, as a loop and as one expression Python · loop_or_vector.py
"""One order total computed twice: with a Python loop and with one NumPy expression, each timed."""
import time

import numpy as np

rng = np.random.default_rng(seed=7)
n = 1_000_000
price_paise = rng.integers(5_000, 50_000, size=n)  # whole paise, so both totals are exact integers
quantity = rng.integers(1, 10, size=n)


def total_with_loop(prices, quantities):
    total = 0
    for p, q in zip(prices, quantities):
        total += p * q
    return total


prices_list, quantities_list = price_paise.tolist(), quantity.tolist()  # plain Python ints for the loop

start = time.perf_counter()
loop_total = total_with_loop(prices_list, quantities_list)
loop_seconds = time.perf_counter() - start

start = time.perf_counter()
vector_total = int((price_paise * quantity).sum())
vector_seconds = time.perf_counter() - start

print(f"{n:,} order lines")
print(f"loop total:       {loop_total:,} paise")
print(f"vectorised total: {vector_total:,} paise")
print("same answer:", loop_total == vector_total)
print(f"loop {loop_seconds * 1000:.0f} ms, vectorised {vector_seconds * 1000:.1f} ms: "
      f"about {loop_seconds / vector_seconds:.0f} times faster on this computer")

Output

1,000,000 order lines
loop total:       137,507,813,345 paise
vectorised total: 137,507,813,345 paise
same answer: True
loop 30 ms, vectorised 1.4 ms: about 22 times faster on this computer

This output changes from run to run: the times depend on the computer and on what else it is doing, so every run prints different milliseconds; the factor always has at least two digits.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 loop_or_vector.py

Both totals are exact, because the prices are whole paise and both versions add integers, so the answers agree to the last digit. On the computer that recorded this run the expression was more than ten times faster, and the last line of the output gives the exact factor. Writing one operation on whole arrays instead of a loop over their elements is called Writing one operation on whole arrays, such as price * quantity, instead of a Python loop over their elements, so that NumPy runs the loop in compiled code.. The loop has not gone away: NumPy’s documentation describes it as running “behind the scenes” in pre-compiled C code. Step through what each version does for its million lines:

Interactive · step by step A million order lines, in a loop and in one expression Step through one pass of the loop on the left, then the NumPy expression on the right. The loop repeats its steps for every order line; NumPy does each of its steps once.
A million order lines, in a loop and in one expression, step 8 of 8. Showing The Python loop, Take p and q, Work out p * q, Make the product, total += product, One NumPy expression, Pick the inner loop, Run the compiled loop, Fill one new array, .sum(). In focus: Fill one new array, .sum().next order lineThe Python loopits body runs 1,000,000 timesTake p and qtwo int objects from listsWork out p * qhow: from the objects' typeMake the producta new int objecttotal += productanother new int objectOne NumPy expressionprice_paise * quantityPick the inner loopint64 times int64, onceRun the compiled loop1,000,000 multiplicationsFill one new array1,000,000 int64 values.sum()a second compiled loop

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. The same total twice. Both versions add up 1,000,000 order lines and get the same exact total, 137,507,813,345 paise. The left column is one pass of the loop over two lists of Python ints; the right column is everything NumPy does.
  2. Two objects per pass. A list holds references to objects, so each pass takes two int objects, a price and a quantity, and names them p and q.
  3. The type decides. An object's type decides what its operations do, so p * q is worked out from the types of p and q: int's own multiplication, done on these two objects.
  4. New objects every pass. Arithmetic returns new number objects: the product is a new int, and total += p * q makes another one for the running total.
  5. And again. Then the loop goes back for the next pair. Every step of the left column runs once per order line, 1,000,000 times in all.
  6. One call. price_paise * quantity is a single call. Every value of an array has the array's one dtype, here int64, so NumPy picks its compiled inner loop for int64 times int64 once, for the whole array.
  7. Raw values, side by side. The compiled loop runs through the two arrays, whose values lie side by side in memory, all the same size, and multiplies 1,000,000 pairs. No Python object is made for any of them.
  8. One array, one sum. The products fill one new int64 array, and .sum() adds them in a second compiled loop. int() turns that one result into a Python int for printing: 137,507,813,345.

The loop repeats its whole column for every order line, making new Python objects on every pass, while NumPy reads the dtype once and runs one compiled loop over raw values. That also explains the conditions of the speed-up. This program doubles numbers three ways, for 5 values and for a million:

Doubling 5 and 1,000,000 numbers three ways Python · small_or_large.py
"""Doubling numbers three ways, for 5 values and for 1,000,000: a list comprehension, NumPy from a list and back, and
NumPy on numbers already in an array. Each is timed with timeit, as the fastest of 5 repeats."""
import timeit

import numpy as np


def per_call(work, loops):
    """Seconds per call: the fastest of 5 repeats of `loops` calls, the steadiest figure on a busy computer."""
    return min(timeit.repeat(work, number=loops, repeat=5)) / loops


times = {}
for n, loops in [(5, 20_000), (1_000_000, 3)]:
    values = list(range(n))  # Python ints in a list, as data often arrives
    ready = np.array(values)  # the same numbers, already in an array
    times[n] = (
        per_call(lambda: [v * 2 for v in values], loops),
        per_call(lambda: (np.array(values) * 2).tolist(), loops),  # list in, list out
        per_call(lambda: ready * 2, loops),
    )
    print(f"{n:,} values, time per call:")
    for name, seconds in zip(["list comprehension", "NumPy from a list and back", "NumPy on a ready array"], times[n]):
        print(f"  {name:<27}{seconds * 1e6:>12,.2f} µs")

small, large = times[5], times[1_000_000]
print("5 values: the comprehension was the fastest of the three:", small[0] < min(small[1], small[2]))
print("1,000,000 values: the conversions cost more than the comprehension:", large[1] > large[0])
print("1,000,000 values: the ready array was over 10 times faster than the comprehension:", large[2] * 10 < large[0])

Output

5 values, time per call:
  list comprehension                 0.08 µs
  NumPy from a list and back         0.65 µs
  NumPy on a ready array             0.31 µs
1,000,000 values, time per call:
  list comprehension            22,153.65 µs
  NumPy from a list and back    35,794.90 µs
  NumPy on a ready array           242.11 µs
5 values: the comprehension was the fastest of the three: True
1,000,000 values: the conversions cost more than the comprehension: True
1,000,000 values: the ready array was over 10 times faster than the comprehension: True

This output changes from run to run: the times depend on the computer and on what else it is doing, so every run prints different microseconds; the three checks at the end hold in every run.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 small_or_large.py

For 5 values the comprehension was the fastest of the three, even against NumPy on a ready array: with so few values, the work NumPy saves is smaller than what each call costs. For a million values, turning the list into an array and back took longer than the whole comprehension, and only on numbers already in an array was NumPy more than ten times faster. Keep data in arrays from the moment it is loaded, and measure speed-ups of your own with the methods of the timeit lesson, reporting the computer that produced them.

np.vectorize() is not vectorisation

np.vectorize(lambda p, q: p * q)(price_paise, quantity) looks vectorised, but NumPy’s documentation says the function is provided for convenience, not for performance, and that it is essentially a for loop: the lambda still runs once per pair. Write the operation on the whole arrays instead: price_paise * quantity.

Checkpoint

Check: Vectorised operations and their conditions

  1. Question 1 of 2 Which line computes the products of the arrays price_paise and quantity in NumPy's compiled loop?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: price_paise * quantity

    Only the expression on whole arrays is vectorised. The other three call Python code once per pair, which is the work the figure showed the loop doing a million times.

  2. Question 2 of 2 A function receives 5 prices as a Python list on every request and must return the doubled prices as a list. What do the recorded runs of this lesson suggest?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Use a list comprehension

    Vectorisation pays off for many values of one dtype that stay in arrays. For a handful of numbers, or when every call converts a list to an array and back, a plain comprehension can be faster: measure before you convert.

Broadcasting lines shapes up from the right

Arithmetic between an array and a single number already works: marks * 2 doubles every mark. NumPy's rule for arithmetic on arrays of different shapes: compare the shapes from the right; two sizes fit when they are equal or one is 1, and a size of 1 stretches. extends that to two arrays whose shapes differ. NumPy’s documentation states the rule:

  • the shapes are compared from the right, one dimension at a time;
  • two sizes are compatible when they are equal or one of them is 1;
  • a missing dimension on the left counts as 1, so (4,) meets (5, 4) as if it were (1, 4);
  • each size of 1 is stretched to the other size, and the result takes the larger size on each axis.

If any pair is incompatible, NumPy raises ValueError: operands could not be broadcast together. The stretching copies nothing: the smaller array’s values are reused for each row or column. The figure puts the rule in order for (5, 4) and (4,), the first pair of shapes in the program below:

The broadcasting rule: align two shapes from the right, pad with 1s, then each pair of sizes must be equal or contain a 1.Two shapes(5, 4) and (4,)Line them up from the right;pad the shorter with 1s:(5, 4) and (1, 4)Every pair of sizesequal, or one ofthem 1?Each axis takes thelarger size: (5, 4)ValueError: operands couldnot be broadcast togetheryesno

How NumPy finds the shape of a result

Text description of the diagram

The diagram is a flow from top to bottom.

  1. Two shapes arrive, for example (5, 4) and (4,).
  2. They are lined up from the right, and the shorter shape is padded on the left with sizes of 1, so (4,) becomes (1, 4).
  3. The decision: is every pair of sizes either equal, or does it contain a 1?
  4. If yes, each axis of the result takes the larger of its two sizes, here (5, 4), and the size-1 axes are stretched without copying data.
  5. If any pair fails, NumPy raises ValueError: operands could not be broadcast together.

The program below applies the rule four times: a row against the table, a column against a row, a bonus that does not fit, and the same bonus turned into a column:

A row against a table, a column against a row, and a shape that does not fit Python · broadcasting.py
"""Broadcasting: a (5, 4) table against a (4,) row, a (3, 1) column against a (4,) row, and shapes that do not fit."""
import numpy as np

marks = np.array(
    [
        [78, 64, 81, 70],
        [55, 72, 49, 88],
        [91, 85, 77, 62],
        [66, 58, 93, 75],
        [42, 90, 68, 81],
    ]
)

# Each mark as a percentage of the top mark in its subject: (5, 4) / (4,) -> (5, 4).
top = marks.max(axis=0)
print("top mark per subject:", top, top.shape)
print((marks / top * 100).round(1))

# A column and a row make a whole table: (3, 1) + (4,) -> (3, 4).
column = np.array([[0], [10], [20]])
row = np.array([1, 2, 3, 4])
table = column + row
print("(3, 1) + (4,) gives", table.shape)
print(table)

# A bonus per student is a column, (5,) -> (5, 1); added as a (5,) row it fails.
bonus = np.array([5, 0, 0, 3, 10])
try:
    marks + bonus
except ValueError as error:
    print("ValueError:", error)
print("with bonus, as a column:", (marks + bonus[:, np.newaxis]).max(axis=1))
print("np.broadcast_shapes((5, 4), (5, 1)) =", np.broadcast_shapes((5, 4), (5, 1)))

Output

top mark per subject: [91 90 93 88] (4,)
[[ 85.7  71.1  87.1  79.5]
 [ 60.4  80.   52.7 100. ]
 [100.   94.4  82.8  70.5]
 [ 72.5  64.4 100.   85.2]
 [ 46.2 100.   73.1  92. ]]
(3, 1) + (4,) gives (3, 4)
[[ 1  2  3  4]
 [11 12 13 14]
 [21 22 23 24]]
ValueError: operands could not be broadcast together with shapes (5,4) (5,) 
with bonus, as a column: [ 86  88  91  96 100]
np.broadcast_shapes((5, 4), (5, 1)) = (5, 4)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 broadcasting.py

Step through the same four operations, one pair of sizes at a time:

Interactive · step by step The rule on four pairs of shapes Step through four operations of broadcasting.py. Each pair of sizes is compared from the right; the result shape, or the error, appears at the bottom.
The rule on four pairs of shapes, step 12 of 12. Showing First array (marks: (5, 4)), Second array (bonus as a column: (5, 1)), Its axis 0 (5), Its axis 0 (5), Its last axis (4), Its last axis (1), Result ((5, 4)). In focus: Result.First arraymarks: (5, 4)Second arraybonus as a column: (5, 1)Its axis 05Its axis 05Its last axis4Its last axis1Result(5, 4)

The drawing shows the whole scene; its 12 steps are listed below.

Every step as text
  1. From the right. marks has shape (5, 4); top, the best mark of each subject, has shape (4,). NumPy compares shapes from the right, so the 4 subjects of marks meet the 4 values of top: equal sizes fit.
  2. A missing axis is 1. top has no axis 0. A missing size counts as 1, as if top had the shape (1, 4).
  3. The 1 stretches. A size of 1 fits any size: it stretches to 5 without copying, so the same four top marks are used for every student.
  4. The result. Each axis of the result takes the larger size: (5, 4). Every mark is divided by the top mark of its subject, so Chen's 91 in maths becomes 100.0 and Asha's 78 becomes 85.7.
  5. A column and a row. column has shape (3, 1) and row (4,). From the right, the 1 of column meets the 4 of row, and the 1 stretches to 4.
  6. Both stretch. On axis 0, row's missing size counts as 1 and stretches to 3. Each array stretches along a different axis.
  7. A whole table. The result is (3, 4): every value of column added to every value of row, from 1 2 3 4 in the first row to 21 22 23 24 in the last. It does the work of a nested loop.
  8. One bonus per student. bonus holds five values, one per student, with shape (5,). From the right, its 5 meets the last axis of marks, the 4 subjects.
  9. Neither is 1. 4 and 5 differ and neither is 1, so NumPy stops with ValueError: operands could not be broadcast together with shapes (5,4) (5,).
  10. Make it a column. bonus[:, np.newaxis] adds an axis after the first, giving shape (5, 1): one row per student and a single column.
  11. Now it fits. From the right: the 1 of the column meets the 4 subjects and stretches, so each student's bonus is used for all four marks. Then 5 meets 5, equal.
  12. The result. The result is (5, 4) again. With each bonus added to all four of the student's marks, the best totals are 86, 88, 91, 96 and 100.

marks.max(axis=0) is a row of four values, the top mark of each subject, so dividing the table by it turns every mark into a percentage of the best mark in its subject, without a loop. A column of shape (3, 1) and a row of shape (4,) stretch in both directions into a whole (3, 4) table, which is the vectorised form of a nested loop. np.broadcast_shapes() gives the result shape of any combination without creating an array; you will write that function yourself in the practice section.

A value per row lines up with the columns

One bonus per student is five values, but as a (5,) array it meets the last axis, the four subjects, so marks + bonus fails. Make it a column first: marks + bonus[:, np.newaxis], or bonus.reshape(-1, 1), has shape (5, 1) and says “one value per row”.

Checkpoint

Check: Broadcasting shapes

  1. Question 1 of 2 What shape does an operation between arrays of shapes (6, 1, 4) and (3, 1) give?

    Choose one answer.

    A hint

    Line the two shapes up from the right, and give the shorter one a 1 on the left.

    Show the answer: Question 1 of 2

    Answer: (6, 3, 4)

    (3, 1) is padded to (1, 3, 1). The pairs are (4, 1), (1, 3) and (6, 1), and each axis takes the larger size: the result has shape (6, 3, 4).

  2. Question 2 of 2 bonus has shape (5,), one value per student, and marks + bonus fails. Which expression adds each student's bonus to all four of their marks?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: marks + bonus[:, np.newaxis]

    A value per row must be a column, shape (5, 1), so that the comparison from the right meets 4 and 1. Adding an axis with np.newaxis, or reshape(-1, 1), does that.

A slice is a view; a mask selection is a copy

Slicing a list copies the selected elements into a new list. Slicing an array does not: it returns a A NumPy array that reads the memory of another array instead of holding its own copy, as a slice such as a[2:5] does. Writing through it changes the original., a new array object that reads the same memory. Selecting with a boolean mask or a list of positions is different again, and returns a copy:

Which changes reach the original array Python · views_and_copies.py
"""A slice of an array is a view of the same data; a boolean selection and .copy() are new arrays."""
import numpy as np

readings = np.array([21.5, 22.0, 35.9, 22.4, 21.8, 36.2])

first_three = readings[:3]  # a view: no data is copied
first_three[0] = 21.6  # so this changes readings as well
print("after editing the slice:    ", readings)

hot = readings[readings > 30]  # a copy: a new array holding the selected values
hot[:] = 0.0  # readings does not change
print("after editing the selection:", readings)

backup = readings.copy()
readings[readings > 30] = np.nan  # assigning through a mask writes into the original
print("readings:", readings)
print("backup:  ", backup)
print("the slice shares memory with readings:", np.shares_memory(first_three, readings))
print("the copy shares memory with readings: ", np.shares_memory(backup, readings))

Output

after editing the slice:     [21.6 22.  35.9 22.4 21.8 36.2]
after editing the selection: [21.6 22.  35.9 22.4 21.8 36.2]
readings: [21.6 22.   nan 22.4 21.8  nan]
backup:   [21.6 22.  35.9 22.4 21.8 36.2]
the slice shares memory with readings: True
the copy shares memory with readings:  False

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 views_and_copies.py

Step through the program and follow the arrow from each name to the block of memory it reads:

Interactive · step by step Which edits reach readings Step through views_and_copies.py. Names are on the left and blocks of memory on the right; an arrow shows which block each name reads.
Which edits reach readings, step 8 of 8. Showing readings, first_three, hot, backup, Block 1: six float64 (21.6 22.0 nan 22.4 21.8 nan), Block 2: two float64 (0.0 0.0), Block 3: six float64. In focus: first_three, backup.its first 3 valuesreadingsnp.array([...])first_threereadings[:3]hotreadings[readings > 30]backupreadings.copy()Block 1: six float6421.6 22.0 nan 22.4 21.8 nanBlock 2: two float640.0 0.0Block 3: six float6421.6 22.0 35.9 22.4 21.8 36.2

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Six readings. readings is an array of six float64 values, stored side by side in one block of memory.
  2. A slice is a view. readings[:3] copies nothing. first_three is a new array object that reads the first three values of the same block: a view.
  3. Writing through the view. first_three[0] = 21.6 writes into block 1, so readings[0] is 21.6 as well. The first line of the output shows it.
  4. A mask selection copies. readings[readings > 30] picks 35.9 and 36.2 into a new block. hot is a new array that reads only that block.
  5. Editing the copy. hot[:] = 0.0 zeroes block 2. Block 1 is untouched, so readings still holds 35.9 and 36.2: the second line of the output.
  6. A copy kept aside. readings.copy() copies all six values into block 3, which no other name reads. That is the raw data, kept before cleaning.
  7. Assigning through a mask. readings[readings > 30] = np.nan is an assignment, not a selection: it writes NaN into block 1 itself, where the mask is True. Block 3 keeps the raw values.
  8. Asking NumPy. np.shares_memory() says True for first_three and readings, which share block 1, and False for backup, whose block 3 is its own.

Changing the slice first_three changed readings, because both read block 1, and np.shares_memory() confirms it. Changing the result of readings[readings > 30] left readings alone, because a mask selection builds a new array. NumPy’s documentation gives the rule in two parts: basic indexing, such as a slice, always makes a view, and advanced indexing, with a mask or a list of positions, always makes a copy. Assigning through a mask, readings[readings > 30] = np.nan, is not a selection at all: the assignment happens in place, in the original. When you need an independent array, for example to keep the raw data before cleaning it, call copy() first.

A slice taken as a backup

raw = readings[:] would be a copy for a list, but for an array it is a view: cleaning readings afterwards changes raw as well. Write raw = readings.copy() before you change anything.

Checkpoint

Check: Views and copies

  1. One question a = np.arange(6) holds 0 1 2 3 4 5. After b = a[2:4] and b[:] = 0, what does a hold?

    Choose one answer.

    Show the answer: One question

    Answer: [0 1 0 0 4 5]

    A slice is basic indexing, which always makes a view: b and a share one block of memory, so the zeros written through b appear in a.

Fixed-size integers wrap around without an error

Python’s int grows as large as a number needs and never overflows. NumPy’s integers are each a An integer type with a set number of bits, such as NumPy's int32, which holds -2,147,483,648 to 2,147,483,647. A result outside that range wraps around.: an int32 holds values from −2,147,483,648 to 2,147,483,647, and the program below doubles three daily view counts stored that way:

int32 against Python int Python · int32_overflow.py
"""Fixed-size NumPy integers wrap around without an error; Python ints never overflow."""
import numpy as np

print(np.iinfo(np.int32))
views = np.array([2_000_000_000, 1_500_000_000, 900_000_000], dtype=np.int32)

print("int32 array * 2:  ", views * 2)  # silently wrong for the first two
print("Python ints * 2:  ", [v * 2 for v in views.tolist()])
print("int64 array * 2:  ", views.astype(np.int64) * 2)
print("sum of the int32 array:", views.sum(), "as", views.sum().dtype)  # sum() widens to int64 here

with np.errstate(over="raise"):  # a setting for floating-point errors and integer scalars, not integer arrays
    print("int32 array * 2 under errstate(over='raise'):", views * 2)

try:
    np.array([3_000_000_000], dtype=np.int32)  # a Python int that does not fit is refused
except OverflowError as error:
    print("OverflowError:", error)

Output

Machine parameters for int32
---------------------------------------------------------------
min = -2147483648
max = 2147483647
---------------------------------------------------------------

int32 array * 2:   [ -294967296 -1294967296  1800000000]
Python ints * 2:   [4000000000, 3000000000, 1800000000]
int64 array * 2:   [4000000000 3000000000 1800000000]
sum of the int32 array: 4400000000 as int64
int32 array * 2 under errstate(over='raise'): [ -294967296 -1294967296  1800000000]
OverflowError: Python integer 3000000000 out of bounds for int32

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 int32_overflow.py

Doubling 2,000,000,000 in an int32 array gave −294,967,296: the true answer, 4,000,000,000, minus 2^32. The same values as Python integers, or as int64, doubled correctly. The documentation of numpy.sum gives the rule: integer arithmetic is modular, and overflow raises no error. Not even np.errstate(over="raise") caught it: that setting covers floating-point errors and integer scalars, and under it the array wrapped exactly as before. Step through what happens to each of the three results:

Interactive · stage by stage How an int32 array stores three doubled view counts Step through views * 2 for each of the three values. The flow is what happens to every result of integer arithmetic on an int32 array.
How an int32 array stores three doubled view counts, step 7 of 7. Showing The exact result (4,000,000,000 in int64), Within the int32 range? (yes: within the int64 range), Wrap around, What the array holds (4,000,000,000). In focus: Within the int32 range?.yes: kept as it isThe exact result4,000,000,000 in int64Within the int32 range?yes: within the int64 rangeWrap aroundminus 2^32, 4,294,967,296What the array holds4,000,000,000

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The exact result. views * 2 doubles every value of the int32 array. For the first view count, 2,000,000,000, the exact result is 4,000,000,000.
  2. Too big for 32 bits. An int32 holds at most 2,147,483,647, and 4,000,000,000 is above that. NumPy checks nothing here: there is no error and no warning.
  3. Wrap around. Integer arithmetic in NumPy is modular. A result above the range loses 2^32: 4,000,000,000 - 4,294,967,296 = -294,967,296.
  4. Silently wrong. The array holds -294,967,296, the first number the program printed. A count of views has turned negative, and the output says nothing about it.
  5. The second count. 1,500,000,000 doubles to 3,000,000,000, also above the limit, and wraps to 3,000,000,000 - 4,294,967,296 = -1,294,967,296.
  6. The third fits. 900,000,000 doubles to 1,800,000,000, which is within the range, so the array keeps it exactly.
  7. Convert first. After views.astype(np.int64), every value has 64 bits and the range reaches 9,223,372,036,854,775,807, so all three doubles fit: 4,000,000,000, 3,000,000,000 and 1,800,000,000.

Two details soften the trap. sum() of a small integer type adds in the platform’s default integer, here int64, which is why the total was right, and NumPy 2 refuses to put a Python integer that does not fit into an int32 array and raises OverflowError. Arithmetic between arrays gets no such check.

The wrapped value can be worked out with Python’s own integers: add or take away 2^32 until the result lies in the range. This program does exactly that and prints the numbers NumPy printed, and also −1,486,618,624, the value NumPy’s documentation shows for 100 ** 9 in 32 bits. Press Edit, put a view count of your own into the list, and run it:

The wrap-around worked out with Python ints Python · wrap/wrap32.py
"""What an int32 keeps of a result, worked out with Python's own integers: the true value, moved into the range
-2**31 .. 2**31 - 1 by adding or taking away 2**32 as often as needed."""
LOW, HIGH = -(2**31), 2**31 - 1


def as_int32(n):
    """The value a 32-bit signed integer holds after arithmetic that gave n."""
    return (n - LOW) % 2**32 + LOW


for views in [2_000_000_000, 1_500_000_000, 900_000_000]:
    true = views * 2
    kept = as_int32(true)
    note = "fits" if LOW <= true <= HIGH else f"{true:,} - {2**32:,}"
    print(f"{views:,} * 2 = {true:,} -> int32 holds {kept:,} ({note})")

print("100 ** 9 in 32 bits:", f"{as_int32(100**9):,}")
print("one past the largest int32:", f"{as_int32(HIGH + 1):,}")

Output

2,000,000,000 * 2 = 4,000,000,000 -> int32 holds -294,967,296 (4,000,000,000 - 4,294,967,296)
1,500,000,000 * 2 = 3,000,000,000 -> int32 holds -1,294,967,296 (3,000,000,000 - 4,294,967,296)
900,000,000 * 2 = 1,800,000,000 -> int32 holds 1,800,000,000 (fits)
100 ** 9 in 32 bits: -1,486,618,624
one past the largest int32: -2,147,483,648

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 wrap32.py

Programmer Calculator (Bitwise) Enter 4000000000, look at its 32-bit pattern, and see why the same bits read as -294967296 in a signed 32-bit integer.

The defence is to choose the dtype on purpose. Keep the default int64 for counts and money in the smallest unit, check the limits with np.iinfo() when data arrives as int32 or int16 (images and files often do), and convert before arithmetic that can grow, as astype(np.int64) did above. When totals must be exact beyond 64 bits, Python integers are the right tool, and for exact decimal amounts the decimal module of the exact arithmetic lesson is.

Arithmetic on int32 without checking the range

steps * 1_000_000 on an int32 array of step counts is right for small counts and silently wrong, even negative, for counts above 2,147: the product leaves the range. Convert first, steps.astype(np.int64) * 1_000_000, or keep such data as int64 from the start.

Checkpoint

Check: Fixed-size integers that wrap around

  1. Question 1 of 2 An int32 array holds 2,000,000,000. What value does the array hold after adding 500,000,000 to it?

    Type a number.

    A hint

    The exact sum is above 2,147,483,647. Take away 2^32, which is 4,294,967,296.

    Show the answer: Question 1 of 2

    Answer: -1794967296

    The exact sum, 2,500,000,000, is above the int32 maximum of 2,147,483,647, so it wraps around: 2,500,000,000 − 4,294,967,296 = −1,794,967,296, with no error and no warning.

  2. Question 2 of 2 views is an int32 array of daily view counts. Which line doubles them with no chance of wrapping around?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: views.astype(np.int64) * 2

    Choose the dtype on purpose before arithmetic that can grow: astype(np.int64) gives every value 64 bits, which holds totals up to 9,223,372,036,854,775,807.

Labelled, mixed tables belong in pandas

NumPy arrays are the right tool when the data is a grid of numbers of one type: measurements, pixels, matrices, simulations. Real tables are often not like that. They have column names, a mix of text, dates and numbers, missing values, and they arrive as CSV or spreadsheet files. That is the work of pandas, which its documentation describes as data structures for “relational” or “labeled” data, built on top of NumPy. Answer the three questions in the table for data of your own:

Interactive · answer and compare A list, a NumPy array or a pandas DataFrame? Answer three questions about your data. The table marks what fits and says why the others do not.
Is every value a number of the same type?
Do you look values up by a column name or a row label?
Will you do arithmetic on many values at once?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Python lists, NumPy arrays and pandas DataFrames against the five things that decide between them
Structure Values Labels Arithmetic on every value Missing values Reading files
Python list Holds: any objects, of any types Good anything, mixed types in any position Poor positions only Poor a loop or a comprehension, one value at a time Mixed whatever you choose, often None Mixed the csv and json modules, row by row
NumPy array Holds: values of one dtype Poor one dtype for the whole array Poor positions only Good vectorised, with broadcasting Mixed NaN, in floating-point arrays only Mixed np.loadtxt for plain numeric text
pandas DataFrame Holds: named columns of their own dtypes Good one dtype per column; columns can differ Good column names and a row index Good vectorised per column, aligned by labels Good handled in every column type, not only floats Good CSV, Excel, JSON, Parquet, SQL databases and more
How the answers decide
  • “Is every value a number of the same type?” No: text, dates and numbers mix: rules out NumPy array. An array has one dtype for every value; text, dates and numbers side by side belong in DataFrame columns, each with its own dtype.
  • “Do you look values up by a column name or a row label?” Yes, by names or labels: rules out Python list and NumPy array. Lists and arrays are indexed by position only; a DataFrame has column names and a row index.
  • “Will you do arithmetic on many values at once?” Yes, on thousands of values or more: rules out Python list. A list needs a Python loop for every value; arrays and DataFrame columns run the loop in compiled code.
  • “Is every value a number of the same type?” Yes, one numeric type and “Do you look values up by a column name or a row label?” No, positions are enough: adds a caveat to pandas DataFrame. A grid of one numeric type, read by position, is what a NumPy array is for; a DataFrame adds labels this data does not need.
  • “Will you do arithmetic on many values at once?” No, a handful of values now and then and “Do you look values up by a column name or a row label?” No, positions are enough: adds a caveat to NumPy array and pandas DataFrame. For a handful of values, creating an array costs more than the loop it saves, as the recorded run of small_or_large.py showed.

In short: a list for small or mixed collections, a NumPy array for numeric grids and the maths on them, and a pandas DataFrame for tables of real-world data. If your next step is analysing tables, continue with the Data Analysis with Python track, which uses pandas 3 and also covers polars and DuckDB. Everything in this lesson carries over: a pandas column is backed by an array, and broadcasting and dtypes behave there as they do here.

Checkpoint

Check: NumPy or pandas

  1. One question A file has one row per order: an order date, the customer's city and an amount, and some cells are empty. What should hold it for analysis?

    Choose one answer.

    Show the answer: One question

    Answer: A pandas DataFrame

    NumPy arrays suit grids of one numeric type. Tables with names, dates, text and gaps belong in a pandas DataFrame, which pandas' documentation describes as built for labelled data, on top of NumPy.

Practise: work out a broadcast shape

The exercise asks for broadcast_shape(*shapes), the rule of this lesson written without NumPy: compare the shapes from the right, let sizes of 1 stretch, and raise ValueError when two sizes differ and neither is 1. Its sample tests run in your browser:

Exercise · Medium · Python

Work out the shape that broadcasting gives

Write broadcast_shape(*shapes) in shapes.py without importing NumPy. It takes any number of shapes, each a tuple of non-negative integers such as (5, 4), (4,) or (), and returns the shape of the result that NumPy's broadcasting rule gives them, as a tuple.

  • Compare the shapes from the right, one axis at a time. A shape with fewer dimensions counts as if it had sizes of 1 on the left.
  • On each axis the sizes must be equal or 1. The result takes the size that is not 1 (or 1, when every size is 1). A size of 0 follows the same rule: it fits with 0 and with 1, and the result is 0.
  • Raise ValueError when any axis has two different sizes that are both not 1.
  • With no shapes at all, return ().

For example, broadcast_shape((5, 4), (4,)) is (5, 4), broadcast_shape((3, 1), (4,)) is (3, 4), and broadcast_shape((5, 4), (5,)) raises ValueError.

Starter code · shapes.py

def broadcast_shape(*shapes):
    """Return the shape that NumPy's broadcasting rule gives these shapes, or raise ValueError."""
    # Replace these lines with your code.
    return shapes[0] if shapes else ()
The sample tests · test_shapes.py
from shapes import broadcast_shape


def refused(*shapes):
    try:
        broadcast_shape(*shapes)
    except ValueError:
        return True
    return False


def test_equal_shapes():
    """two equal shapes give the same shape"""
    assert broadcast_shape((5, 4), (5, 4)) == (5, 4)
    assert broadcast_shape((7,), (7,)) == (7,)


def test_row_against_table():
    """a shorter shape is padded with 1s on the left, in either order"""
    assert broadcast_shape((5, 4), (4,)) == (5, 4)
    assert broadcast_shape((4,), (5, 4)) == (5, 4)
    assert broadcast_shape((6, 5, 4), (5, 1)) == (6, 5, 4)


def test_column_against_row():
    """sizes of 1 stretch in both arrays"""
    assert broadcast_shape((3, 1), (4,)) == (3, 4)
    assert broadcast_shape((2, 1, 4, 1), (3, 1, 5)) == (2, 3, 4, 5)


def test_incompatible_shapes():
    """two different sizes that are not 1 raise ValueError"""
    assert refused((5, 4), (5,)), "(5, 4) and (5,) do not broadcast"
    assert refused((2, 3), (3, 2)), "(2, 3) and (3, 2) do not broadcast"
    assert refused((1, 2), (3, 1), (4, 1)), "3 and 4 meet on the first axis"


def test_scalars_and_many_shapes():
    """() is a scalar, no shapes give (), and any number of shapes combine"""
    assert broadcast_shape((), (3,)) == (3,)
    assert broadcast_shape() == ()
    assert broadcast_shape((1, 3), (2, 1), (1,)) == (2, 3)


def test_size_zero():
    """0 fits with 0 and with 1, never with another size"""
    assert broadcast_shape((0,), (1,)) == (0,)
    assert broadcast_shape((2, 0), (1, 0)) == (2, 0)
    assert refused((0,), (3,)), "0 and 3 do not broadcast"
A hint

Work out how many dimensions the result has first: the length of the longest shape. Then loop over the axes from the right with for i in range(1, ndim + 1) and look at shape[-i] in every shape that is long enough. Start each axis at size 1; a size of 1 changes nothing, a first other size becomes the axis's size, and any later size that differs from it is an error. Collect the sizes and reverse them at the end.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

After the exercise, run through the seven ideas once more, from indexing to pandas. The questions begin with the idea your checkpoints found shakiest, and a wrong pick is answered with the slip behind it:

Practice that adapts to you

Practise this lesson

20 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 20 An array has 5 rows and 4 columns. What are its ndim and its size?

    Choose one answer.

    Show the answer: Question 1 of 20

    Answer: 2 and 20

    shape is (5, 4), ndim is 2 because there are two axes, and size is 20 because 5 × 4 values are stored.

  2. Question 2 of 20 What does marks[marks > 90] give for the marks table of this lesson?

    Choose one answer.

    Show the answer: Question 2 of 20

    Answer: [91 93]

    marks > 90 is a boolean array of the table's shape, True at two places. marks[marks > 90] selects those values into a new array: [91 93].

  3. Question 3 of 20 The rows of marks are students. Which call gives one mean per student?

    Choose one answer.

    Show the answer: Question 3 of 20

    Answer: marks.mean(axis=1)

    The axis you pass is the one that disappears. To keep one value per student, axis 0, remove axis 1, the subjects: marks.mean(axis=1) gives 73.25, 66.0, 78.75, 73.0 and 70.25.

  4. Question 4 of 20 marks has shape (5, 4): five students, four subjects. What is the shape of marks.mean(axis=0)?

    Choose one answer.

    Show the answer: Question 4 of 20

    Answer: (4,), one mean per subject

    The axis you pass is the one that disappears. Reducing over axis 0, the rows, leaves one value for each of the four columns, so the result has shape (4,).

  5. Question 5 of 20 sales has shape (7, 24): a shop's takings for each day of a week (rows) and each hour of the day (columns). Which expression gives, for each day, the hour with the highest takings?

    Choose one answer.

    Show the answer: Question 5 of 20

    Answer: sales.argmax(axis=1)

    Reduce along the axis you want to collapse, the hours, and choose argmax because you want the position, not the value: sales.argmax(axis=1), one hour for each of the 7 days.

  6. Question 6 of 20 What does NumPy's documentation say about np.vectorize()?

    Choose one answer.

    Show the answer: Question 6 of 20

    Answer: It is there for convenience, not performance, and is essentially a for loop

    np.vectorize is a convenience: it lets a function written for single values accept arrays and broadcast them, but the function still runs once per element in Python.

  7. Question 7 of 20 Both versions in loop_or_vector.py multiply 1,000,000 pairs and add up the products. Why was the loop the slower one?

    Choose one answer.

    Show the answer: Question 7 of 20

    Answer: Every pass takes two int objects, works out the multiplication from their type and makes new int objects, a million times over

    The loop's body runs in the interpreter once per order line, making new objects on every pass. NumPy picks one compiled inner loop for int64 times int64 and runs it over all the values at once.

  8. Question 8 of 20 In small_or_large.py, which way of doubling 1,000,000 numbers that arrive as a Python list and must leave as one was faster: a list comprehension, or converting to an array, multiplying and converting back?

    Choose one answer.

    Show the answer: Question 8 of 20

    Answer: The list comprehension

    Vectorisation saves the loop, but converting a list to an array, and back, is a loop of its own. Keep data in arrays from the moment it is loaded, so that every operation on it runs in compiled code.

  9. Question 9 of 20 In NumPy's broadcasting rule, when do two sizes on the same axis fit?

    Choose one answer.

    Show the answer: Question 9 of 20

    Answer: When they are equal, or one of them is 1

    NumPy compares the shapes from the right. Two sizes are compatible when they are equal or one of them is 1; otherwise it raises ValueError: operands could not be broadcast together.

  10. Question 10 of 20 Which pairs of shapes broadcast together without an error?

    Choose every answer that is right.

    Show the answer: Question 10 of 20

    Answer:

    • (6, 2, 3) and (2, 1)
    • (3, 1) and (1, 4)
    • (5, 4) and (4,)

    Compare from the right. (4,) meets the last axis of (5, 4) and fits; (5,) meets the 4 and does not. (3, 1) and (1, 4) give (3, 4). (2, 1) is padded to (1, 2, 1) and gives (6, 2, 3). In (2, 3) and (3, 2) the last sizes, 3 and 2, differ and neither is 1.

  11. Question 11 of 20 temps has shape (365, 24): a year of hourly temperatures, one row per day. Which expression subtracts each day's own mean from that day's 24 readings?

    Choose one answer.

    Show the answer: Question 11 of 20

    Answer: temps - temps.mean(axis=1)[:, np.newaxis]

    A reduction along axis 1 gives one value per row, shape (365,). To use it per row against a (365, 24) table it must be a column, (365, 1), exactly as the bonus had to be.

  12. Question 12 of 20 Which of these gives a view of the array a, not a copy?

    Choose one answer.

    Show the answer: Question 12 of 20

    Answer: a[1:4]

    NumPy's documentation puts it in two rules: basic indexing, such as a slice, always makes a view; advanced indexing, with a list of positions or a mask, always makes a copy.

  13. Question 13 of 20 What does views_and_copies.py print?

    What does this program print? Choose one answer.

    """A slice of an array is a view of the same data; a boolean selection and .copy() are new arrays."""
    import numpy as np
    
    readings = np.array([21.5, 22.0, 35.9, 22.4, 21.8, 36.2])
    
    first_three = readings[:3]  # a view: no data is copied
    first_three[0] = 21.6  # so this changes readings as well
    print("after editing the slice:    ", readings)
    
    hot = readings[readings > 30]  # a copy: a new array holding the selected values
    hot[:] = 0.0  # readings does not change
    print("after editing the selection:", readings)
    
    backup = readings.copy()
    readings[readings > 30] = np.nan  # assigning through a mask writes into the original
    print("readings:", readings)
    print("backup:  ", backup)
    print("the slice shares memory with readings:", np.shares_memory(first_three, readings))
    print("the copy shares memory with readings: ", np.shares_memory(backup, readings))
    Show the answer: Question 13 of 20

    Answer: it prints

    after editing the slice:     [21.6 22.  35.9 22.4 21.8 36.2]
    after editing the selection: [21.6 22.  35.9 22.4 21.8 36.2]
    readings: [21.6 22.   nan 22.4 21.8  nan]
    backup:   [21.6 22.  35.9 22.4 21.8 36.2]
    the slice shares memory with readings: True
    the copy shares memory with readings:  False

    readings[:3] is a view, so writing 21.6 into it changes readings. readings[readings > 30] is a new array, so zeroing it changes nothing. Assigning through the mask writes NaN into readings itself, and the backup made with copy() keeps the old values.

  14. Question 14 of 20 You must clean readings in place, replacing values above 30 with NaN, and keep the raw values for a report. Which code does both?

    Choose one answer.

    Show the answer: Question 14 of 20

    Answer: raw = readings.copy(), then readings[readings > 30] = np.nan

    Copy before you change: only copy() gives raw its own memory, and it must run before the assignment that writes into readings.

  15. Question 15 of 20 a = np.array([2_000_000_000], dtype=np.int32). What does a * 2 give?

    Choose one answer.

    Show the answer: Question 15 of 20

    Answer: array([-294967296], dtype=int32), wrapped around without an error

    Arithmetic on fixed-size integer arrays is modular: 4,000,000,000 does not fit into 32 bits, so the result is 4,000,000,000 minus 2^32. NumPy raises OverflowError only when a Python int that does not fit is put into an int32 array.

  16. Question 16 of 20 What does Python's own int give for 2_000_000_000 * 2?

    Choose one answer.

    Show the answer: Question 16 of 20

    Answer: 4000000000

    Python's int is flexible in size, as NumPy's documentation contrasts it with NumPy's integers, so the product is exactly 4,000,000,000.

  17. Question 17 of 20 Why did views.sum() of the int32 array give the right total, 4,400,000,000, while views * 2 wrapped around?

    Choose one answer.

    Show the answer: Question 17 of 20

    Answer: sum() adds a small signed integer type in the platform's default integer, int64 here

    The result of sum() on an int32 array is accumulated in the platform integer, int64 on this computer, so the total of the three counts fits. Element-wise arithmetic keeps the array's own dtype.

  18. Question 18 of 20 An image is a uint8 array, whose values run from 0 to 255. What does adding 10 to a pixel of 250 give?

    Choose one answer.

    Show the answer: Question 18 of 20

    Answer: 4

    The same rule as int32, with 8 bits: the result is 260 − 256 = 4. Convert to a wider type before arithmetic that can leave the range.

  19. Question 19 of 20 Which data is a better fit for pandas than for a plain NumPy array?

    Choose one answer.

    Show the answer: Question 19 of 20

    Answer: A CSV of orders with a date, a customer name, a city and an amount per row, and some empty cells

    Named columns of different types, dates and missing values are what pandas DataFrames are built for. Grids of one numeric type, such as images, simulations and coordinates, are what NumPy arrays are for.

  20. Question 20 of 20 What does the pandas documentation say pandas is built on?

    Choose one answer.

    Show the answer: Question 20 of 20

    Answer: NumPy

    The pandas overview describes pandas as built on top of NumPy, with data structures for labelled data. What you learned here about dtypes and vectorisation applies to its columns.

Interview questions

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

A list is a sequence of references to Python objects of any type, and it can grow and shrink. A NumPy array is a block of memory holding values of one dtype, such as int64 or float64, side by side, and its size is fixed when it is created. That layout lets NumPy apply an operation to every element in compiled code, so a * 2 doubles a million numbers without a Python loop and is usually tens of times faster than a comprehension, and it lets arrays of different shapes combine through broadcasting. Arrays also behave differently: arithmetic on them is element-wise ([1, 2] * 2 repeats a list, while an array doubles its values), slicing returns a view instead of a copy, and fixed-size integer types can overflow. Use lists for small or mixed collections, arrays for large numeric data.

Key takeaways

  • An ndarray holds values of one dtype in one block of memory, read with one position or slice per axis and selected with boolean masks.
  • A reduction removes the axis you pass: axis=0 gives one result per column, axis=1 one per row, and no axis one value for the whole array.
  • Whole-array operations run their loop in compiled code, more than ten times faster than a Python loop in the recorded runs, but not for a handful of values or when every call converts a list to an array and back.
  • Broadcasting compares shapes from the right, so sizes must be equal or 1, a missing size counts as 1, and a value per row needs the shape (n, 1).
  • Slices are views of the same memory, while mask selections and copy() make new arrays, and assigning through a mask writes into the original.
  • int32 and smaller types wrap around silently, so totals belong in int64 or Python’s int, and labelled, mixed tables belong in pandas.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.