Section 1 of 13

1 Before you start

Before you begin, take a few minutes to check what you already know and how confident you feel. You will see the same questions again at the end of the module — this helps both you and us measure what you have learned. Click an option for every question and confidence rating, then click Next to continue.

Pre-test

What happens when you write arr.shape() with parentheses, on a NumPy array?

Pre-test

What is the dtype of np.array([54, 61, 72.5])?

Pre-test

What does np.arange(0, 10, 2) produce?

Pre-test

You have arr = np.array([1, 2, 3, 4]). What does arr * 2 produce?

Pre-test

What does np.sqrt(np.array([1, 4, 9, 16])) return?

Pre-test

For ages = np.array([54, 61, 47, 72, 38]), what does ages[1:4] return?

Pre-test

You have a 2D array m with shape (3, 4). What does m[:, 0] return?

Pre-test

For ages = np.array([54, 61, 47, 72, 38]), what shape does ages.reshape(-1, 1) give?

Pre-confidence

I can build a NumPy array from a Python list and read its shape, dtype, and size attributes.

Not at all confident
Fully confident
Pre-confidence

I can do element-wise arithmetic on a NumPy array, index single values and slices of a 1D or 2D array, and reshape an array into a different shape.

Not at all confident
Fully confident
Section 2 of 13

2 Introduction

While standard Python lists are great for basic data storage, they struggle with heavy numerical work. If you want to modify a list of 1,000 patient ages or find their average, standard Python requires writing slow loops or using extra modules step-by-step.

NumPy is the package that solves this. While a NumPy array looks like a standard Python list on the outside, underneath it is a tightly packed data grid designed specifically for high-speed math.

Instead of writing clunky loops and multiple lines of code, NumPy lets you perform heavy calculations with a single expression. With just one line, you can add five to every age, multiply entire datasets together, calculate the average of a million numbers, or instantly filter for patients over 65.

This first part of the NumPy module covers the foundations you reach for before any analysis:

  • Introduction to NumPy arrays - what an array is, how to create one, and the three attributes (shape, dtype, size) that describe it.
  • Array operations and indexing - element-wise arithmetic, picking out single values or slices, and the way 2D arrays use a row, column index.

Try every snippet in the Python Scratchpad on the right. By the end of this part you will build NumPy arrays from data, perform vectorised arithmetic, and index 1D and 2D arrays with positions and slices.

Section 3 of 13

3 Introduction to NumPy arrays

NumPy is a third-party package, so the first step is to make it available to your script. The convention is to import it under the alias np. From that point on, every NumPy tool is reached as np.something.

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import numpy as np
ages = np.array([54, 61, 47, 72, 38])
print(ages)

np.array takes a regular Python list and turns it into a NumPy array. When you print the result, you will see something that looks like a list with no commas: [54 61 47 72 38]. That spacing is NumPy's way of showing you the value is an array, not a list. From here on you can do array things to it - add a number to every element, take the mean, slice it - in a way you cannot do to a plain list.

Every NumPy array carries three pieces of information about itself that you can read with the dot syntax:

  • shape - a tuple describing how many elements there are along each dimension. For a 1D array of five numbers, the shape is (5,). The trailing comma is Python's way of writing a one-element tuple.
  • dtype - the type of value inside the array. All elements of a NumPy array share the same type, so dtype is one value, not a list. Common ones are int64 (whole numbers), float64 (decimals), and bool.
  • size - the total number of elements. For a 1D array this is the same as len(arr).
Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import numpy as np
weights = np.array([72.5, 68.0, 80.2, 65.4])
print(weights.shape)
print(weights.dtype)
print(weights.size)

Notice that shape, dtype, and size do not have round brackets after them.

This is because shape, dtype, and size are attributes (stored data) rather than methods (functions), they don't take round brackets. Writing weights.shape() is a classic beginner mistake that will cause Python to throw a "tuple object is not callable" error.

Predict the output

Read the code carefully and type what you think it will print. Click Submit prediction for AI tutor feedback comparing your prediction against the real output, then click Reveal actual output to run the snippet yourself and see what happens.

Code
import numpy as np
ages = np.array([54, 61, 47, 72, 38])
print(ages.size)

So far we have built arrays from a list we typed in. NumPy also gives you a few constructors for making arrays without typing the values one by one. These come up constantly when you need a block of placeholder values to fill in later, or a regular sequence of numbers for an axis of a plot.

  • np.zeros(n) - an array of n zeros. np.zeros(5) is [0. 0. 0. 0. 0.].
  • np.ones(n) - an array of n ones. np.ones(3) is [1. 1. 1.].
  • np.arange(start, stop, step) - like Python's built-in range, but it returns an array. np.arange(0, 10, 2) is [0 2 4 6 8].
  • np.linspace(start, stop, n) - n evenly spaced values from start to stop, both ends included. np.linspace(0, 1, 5) is [0. 0.25 0.5 0.75 1. ].
  • Here is an image to illustrate np.zeros and np.ones
Use np.zeros(n) or np.ones(n) to make an array of n identical values.
Use np.zeros(n) or np.ones(n) to make an array of n identical values.
  • …and np.arange and np.linspace.
np.arange(start, stop, step) walks in fixed steps and stops before the end, while np.linspace(start, stop, n) picks n evenly spaced values including both ends.
np.arange(start, stop, step) walks in fixed steps and stops before the end, while np.linspace(start, stop, n) picks n evenly spaced values including both ends.
  • Try the following to see these functions in action:
Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import numpy as np

print(np.zeros(4))
print(np.ones(3))
print(np.arange(0, 10, 2))
print(np.linspace(0, 1, 5))
Section 4 of 13

4 Random number generation with np.random

Many data analyses rely on random numbers for tasks like generating fake datasets, shuffling rows, or sampling patients. To handle this, use NumPy's np.random module.

The modern best practice is to create a random number generator using np.random.default_rng(seed). Setting a "seed" makes your output reproducible so that your code will produce the exact same sequence of "random" numbers every time it runs. This is a crucial step to ensure you can repeat your analysis. Try it out!

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import numpy as np
rng = np.random.default_rng(seed=42)
ages = rng.integers(20, 90, size=10)
print(ages)
shuffled = rng.permutation(ages)
print(shuffled)
Fixing the seed makes two runs return the exact same numbers, while leaving it unset makes each run draw a fresh, different sequence, which is the difference between a reproducible analysis and one you cannot repeat.
Fixing the seed makes two runs return the exact same numbers, while leaving it unset makes each run draw a fresh, different sequence, which is the difference between a reproducible analysis and one you cannot repeat.
Section 5 of 13

5 2D or 3D NumPy arrays

Arrays are not limited to one dimension. A 2D array is a grid of values - rows and columns - which is exactly the shape your data takes when you have several measurements per patient. You build a 2D array from a list of lists, where each inner list becomes a row.

A 3D array is built by stacking 2D grids on top of each other, so a third index k is needed to pick which grid before using the familiar row index i and column index j.
A 3D array is built by stacking 2D grids on top of each other, so a third index k is needed to pick which grid before using the familiar row index i and column index j.
Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import numpy as np
patients = np.array([
    [54, 72.5, 1.75],
    [61, 68.0, 1.80],
    [47, 80.2, 1.68]
])
print(patients)
print(patients.shape)
A list of lists becomes a 2D array where each inner list is one row, and .shape reports its size as (rows, columns).
A list of lists becomes a 2D array where each inner list is one row, and .shape reports its size as (rows, columns).

The shape of this array is (3, 3) - three rows, three columns. When you print it, NumPy lays it out as a grid so you can read the rows and columns at a glance. Notice that even though the ages were whole numbers, NumPy has shown them as 54., 61., 47. with a trailing dot. That is because every element of an array shares one dtype, and once a single float (like 72.5) sits in the data, NumPy promotes the whole array to float64.

One final, crucial point: while NumPy arrays and Python lists look similar, they have very different rules.

Here is what you need to remember:

  • Data Types: A Python list can hold a mix of anything (a string, an integer, and another list all together). A NumPy array is strict—every element must be the exact same type (a shared dtype).
  • Growing Data: A list can easily grow as you append new items. An array has a fixed size. To make it bigger, you actually have to build a brand-new, larger array.
  • Extensive Operations: The powerful tools we will cover next—like instant math, fast statistics, and simple filtering—only work on arrays, not lists.
Lists and NumPy arrays look similar, but the same operator behaves differently on each.
Lists and NumPy arrays look similar, but the same operator behaves differently on each.
Section 6 of 13

6 Array broadcasting and operations

Imagine you have an array of blood glucose readings from a hundred patients, and you need to convert each one from mg/dL to mmol/L (dividing by 18).

With a plain Python list, you would write a loop: step through each value, divide it, append it to a new list. Slow and verbose.

NumPy's shortcut is broadcasting. Instead of looping, you write a single expression for the entire array. When you divide the array by 18, NumPy "broadcasts" that lone number across every position so the mathematical shapes line up, converting all 100 readings instantly.

A Python loop applies divide-by-18 to one array element per iteration, whereas NumPy broadcasting expresses the same divisor against the whole array in a single line that converts every value at once.
A Python loop applies divide-by-18 to one array element per iteration, whereas NumPy broadcasting expresses the same divisor against the whole array in a single line that converts every value at once.
Section 6.1 of 13

6.1 Element-wise arithmetic

The first big payoff of using NumPy is element-wise arithmetic. Add a number to an array and the addition happens once for every element, with no loop in sight. The same is true for subtraction, multiplication, division, and exponentiation.

NumPy applies arithmetic to each pair of elements at the same position, and a single number is reused for every element.
NumPy applies arithmetic to each pair of elements at the same position, and a single number is reused for every element.
Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import numpy as np
ages = np.array([54, 61, 47, 72, 38])
print(ages + 5)
print(ages * 2)

When you write ages + 5, NumPy instantly creates a brand-new array where 5 is added to every single element (leaving you with [59, 66, 52, 77, 43]). Don't worry—your original ages array is left completely untouched! Similarly, typing ages * 2 simply doubles every number.

Broadcasting takes that single number (like the 5) and "broadcasts" it across every position in your array.

The best part? You didn't have to write a single for loop to make this happen. Behind the scenes, NumPy runs all of this using fast, compiled code. If you were working with a dataset of a million values, this method would be significantly faster than running a manual loop on a standard Python list.

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import numpy as np
weights = np.array([72.5, 68.0, 80.2])
heights = np.array([1.75, 1.80, 1.68])
bmi = weights / (heights ** 2)
print(bmi)
NumPy applies arithmetic across whole arrays at once, broadcasting a scalar to every element or pairing two equal-shaped arrays position by position, with no for loop.
NumPy applies arithmetic across whole arrays at once, broadcasting a scalar to every element or pairing two equal-shaped arrays position by position, with no for loop.
Section 6.2 of 13

6.2 Broadcasting beyond arithmetic

You've already seen that adding a single number to an array adds it to every element. The same idea extends to two arrays, as long as their shapes are compatible.

Take a (5, 3) array of patient measurements: five patients down the rows, three variables across the columns. The mean of each column gives you a row of three values, shape (3,). Subtracting that row from the full array centres every column in one expression, because NumPy lines the two shapes up from the right and stretches the single row down all five rows.

The general rule is that two arrays can broadcast when each pair of dimensions is either equal or one of them is 1.

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import numpy as np
patients = np.array([
    [54, 72.5, 1.75],
    [61, 68.0, 1.68],
    [47, 80.2, 1.82],
])
column_means = patients.mean(axis=0)
print(column_means)
centred = patients - column_means
print(centred)
Two arrays can broadcast only if their shapes line up from the right with every pair either equal or one of them 1, which is the test that decides whether an operation like mean-centring is allowed at all.
Two arrays can broadcast only if their shapes line up from the right with every pair either equal or one of them 1, which is the test that decides whether an operation like mean-centring is allowed at all.

NumPy also gives you a stack of mathematical functions that behave the same way - apply once to the whole array, return a new array of the same shape. np.sqrt, np.log, np.exp, and np.abs are the ones you will meet first. Each one runs the function on every element.

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import numpy as np
values = np.array([1, 4, 9, 16, 25])
print(np.sqrt(values))
Section 6.3 of 13

6.3 Math functions: np.log and np.exp

Alongside np.sqrt you will often reach for np.log (natural logarithm) and np.exp (e raised to a power). Both behave element-wise like every other NumPy function - one call works on the whole array. They show up whenever you transform skewed lab values to a more symmetric scale (log-transform), or convert a log-odds back to a probability.

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import numpy as np
values = np.array([1, 10, 100, 1000])
print(np.log(values))
print(np.exp([0, 1, 2]))
np.log and np.exp each apply to every element of an array from a single call, so element-wise behaviour is a property of NumPy functions in general, not of one specific function.
np.log and np.exp each apply to every element of an array from a single call, so element-wise behaviour is a property of NumPy functions in general, not of one specific function.
Section 7 of 13

7 Array indexing

Pulling a single value out of a 1D array uses the same square-bracket syntax as a list. The first element is at index 0, the last at index -1. A slice arr[a:b] gives you a smaller array from index a up to but not including b - the same rule lists follow.

NumPy 1D indexing, showing how positions, negative indices, and slices select elements from an array.
NumPy 1D indexing, showing how positions, negative indices, and slices select elements from an array.
Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import numpy as np
ages = np.array([54, 61, 47, 72, 38])
print(ages[0])
print(ages[-1])
print(ages[1:4])

ages[0] is 54, the first element. ages[-1] is 38, the last. ages[1:4] is the slice [61 47 72] - elements at indices 1, 2, and 3 (4 is excluded). A slice of an array is itself a NumPy array, so all the same operations are available on it.

For 2D arrays the indexing is slightly richer because you have two axes to address. The shape is (rows, columns) and the indexing follows the same order: arr[row, column]. A colon on either side means "all of that axis".

NumPy 2D indexing, showing how row and column positions select scalars, rows, columns, and submatrices from a grid.
NumPy 2D indexing, showing how row and column positions select scalars, rows, columns, and submatrices from a grid.
Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import numpy as np
patients = np.array([
    [54, 72.5, 1.75],
    [61, 68.0, 1.80],
    [47, 80.2, 1.68]
])
print(patients[0, 1])
print(patients[0, :])
print(patients[:, 0])

patients[0, 1] is the value in row 0, column 1 - the weight of the first patient, 72.5. patients[0, :] is the whole first row - all three values for the first patient. patients[:, 0] is the whole first column - the ages of all three patients. Once you have data in this shape, the colon trick is how you ask questions like "give me every patient's BMI" or "give me the third measurement for every patient."

A common trip-up: with a list of lists in plain Python you would write grid[1][2] - two pairs of brackets, indexing one list out of the outer list and then one element out of that. With a NumPy array the comma form grid[1, 2] is the idiomatic way. grid[1][2] still works on most arrays, but the comma form is faster, more flexible (it gives you slices like [:, 0] for free), and what you will see in real code.

Chained indexing walks through an intermediate row to reach an element, while the comma form addresses the (row, column) cell directly in one step.
Chained indexing walks through an intermediate row to reach an element, while the comma form addresses the (row, column) cell directly in one step.

One more useful tool is reshape, which gives you a view of an array with the same data laid out in a different shape. Reshaping is handy when you have a long 1D array of numbers but really they should be thought of as a grid - say twelve readings that are three patients with four measurements each.

Reshape keeps the same data in the same order, and only changes how it is grouped into rows and columns.
Reshape keeps the same data in the same order, and only changes how it is grouped into rows and columns.
Predict the output

Read the code carefully and type what you think it will print. Click Submit prediction for AI tutor feedback comparing your prediction against the real output, then click Reveal actual output to run the snippet yourself and see what happens.

Code
import numpy as np
x = np.arange(12)
y = x.reshape(3, 4)
print(y.shape)

The product of the dimensions in the new shape has to equal the total number of elements - 3 * 4 = 12, which matches np.arange(12). If the numbers do not match, NumPy raises a ValueError telling you so.

Section 7.1 of 13

7.1 The reshape(-1, 1) idiom

A 1D array of n values has shape (n,). Many downstream tools want a 2D column vector of shape (n, 1) instead - one column with n rows. The trick is .reshape(-1, 1). The -1 tells NumPy to work out that dimension itself given the total number of elements, so you do not have to count.

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import numpy as np
ages = np.array([54, 61, 47, 72, 38])
print(ages.shape)
column = ages.reshape(-1, 1)
print(column)
print(column.shape)
reshape(-1, 1) turns a flat array into a single-column 2-D vector, and the -1 is not a size but an instruction for NumPy to infer that axis from the total number of elements.
reshape(-1, 1) turns a flat array into a single-column 2-D vector, and the -1 is not a size but an instruction for NumPy to infer that axis from the total number of elements.
Section 8 of 13

8 Check your understanding

You have reached the end of the module. Try the same questions again — your answers here, paired with your pre-test answers, are how we measure what the module taught you. Answer every question and confidence rating, then click Submit and see results to view your score.

Post-test

What happens when you write arr.shape() with parentheses, on a NumPy array?

Post-test

What is the dtype of np.array([54, 61, 72.5])?

Post-test

What does np.arange(0, 10, 2) produce?

Post-test

You have arr = np.array([1, 2, 3, 4]). What does arr * 2 produce?

Post-test

What does np.sqrt(np.array([1, 4, 9, 16])) return?

Post-test

For ages = np.array([54, 61, 47, 72, 38]), what does ages[1:4] return?

Post-test

You have a 2D array m with shape (3, 4). What does m[:, 0] return?

Post-test

For ages = np.array([54, 61, 47, 72, 38]), what shape does ages.reshape(-1, 1) give?

Post-confidence

I can build a NumPy array from a Python list and read its shape, dtype, and size attributes.

Not at all confident
Fully confident
Post-confidence

I can do element-wise arithmetic on a NumPy array, index single values and slices of a 1D or 2D array, and reshape an array into a different shape.

Not at all confident
Fully confident
Section 9 of 13

9 Your results

Here is how your post-test answers compare with your pre-test answers. The pre/post pairing is the most reliable way to see what this module actually taught you.

Your score

Submit the post-test to see your results.

Muddiest point

What is the one thing from this module that is still unclear to you?

Rate this module

Overall, how would you rate this module?

How likely are you to recommend this module to a peer? (0 = not at all, 10 = extremely likely)