Home › Practice Tools › NumPy Simulator
๐Ÿ
Python / NumPy

NumPy Simulator

A real Python + NumPy notebook running in your browser - write and run actual array code, no install needed. Save and share your notebook.
Ideal for: Data-analysis beginners who want to write and run real NumPy array code without setting up Python locally.

What you'll practice

Start practicing

NumPy Simulator is part of Practice Tools. Sign in or create an account, then subscribe for โ‚ฆ4,000/month to unlock it.

๐Ÿ“‹ Lab Instructions

Step-by-step exercises for NumPy Simulator - read them here, then practice live in the tool above.

Beginner

Last updated: August 22, 2026
NumPy โ€” Beginner Lab Instructions | CRMNuggets
Practice Tools & Labs โ€บ NumPy โ€บ Beginner
๐ŸŸข Beginner

NumPy โ€” Beginner Track

A full walkthrough of installing NumPy, understanding why arrays exist, indexing/slicing, basic array math, and writing your first small analysis script.

Basic Python syntax (variables, lists, loops) Python 3.9+ pip installed 45โ€“60 min lab

1. What NumPy actually is

NumPy (Numerical Python) is the foundational array-computing library nearly every other Python data/science tool is built on top of โ€” pandas, scikit-learn, TensorFlow, and PyTorch all either use NumPy arrays directly or mirror its API. Its core contribution is a single data structure, the ndarray (n-dimensional array), plus a huge set of functions that operate on it far faster than equivalent pure-Python loops. The reason it's fast: a NumPy array stores all its elements as one contiguous, fixed-type block of memory and pushes loops down into pre-compiled C code, instead of looping element-by-element in Python the way a plain list does.

The beginner track gets you from "never imported numpy" to comfortably creating arrays, indexing/slicing them, and doing basic elementwise math โ€” the foundation everything later builds on.

2. Environment setup

  1. Confirm Python is installed: run python3 --version in a terminal (3.9 or newer is fine).
  2. Install NumPy: pip install numpy (use pip3 if that's how your system distinguishes Python versions).
  3. Open a Python REPL or a notebook โ€” either a plain python3 interactive shell, a Jupyter notebook (pip install notebook, then jupyter notebook), or any code editor with a Python runner works fine for this track.
  4. Import NumPy using its standard alias โ€” every example in this guide and virtually all NumPy code you'll ever read assumes this convention:
import numpy as np

3. Why arrays instead of lists

A Python list can hold mixed types and grows/shrinks freely, but that flexibility is exactly what makes it slow for numeric work โ€” Python has to check each element's type individually. A NumPy array requires every element to be the same type and a fixed shape, which is what lets NumPy vectorize operations (apply them to the whole array at once, in compiled code) instead of looping in Python.

import numpy as np

# A Python list requires a manual loop or comprehension to do math on every element
prices = [10, 20, 30, 40]
doubled = [p * 2 for p in prices]

# A NumPy array does the same thing directly โ€” no loop needed
prices_arr = np.array([10, 20, 30, 40])
doubled_arr = prices_arr * 2   # array([20, 40, 60, 80])
That last line โ€” prices_arr * 2 โ€” is called vectorization, and it's the single most important concept in this entire guide. Whenever you catch yourself writing a for loop over a NumPy array to do simple math, there is almost always a vectorized alternative that's both shorter and dramatically faster.

4. Creating arrays

import numpy as np

# From a Python list
a = np.array([1, 2, 3, 4, 5])

# From a nested list โ€” becomes a 2D array (a matrix)
b = np.array([[1, 2, 3], [4, 5, 6]])

# Built-in generators โ€” avoid typing values by hand
np.zeros(5)              # array([0., 0., 0., 0., 0.])
np.ones((2, 3))          # a 2x3 array of 1.0
np.arange(0, 10, 2)      # array([0, 2, 4, 6, 8]) โ€” like Python's range()
np.linspace(0, 1, 5)     # 5 evenly spaced values from 0 to 1, inclusive

Inspecting an array

a = np.array([[1, 2, 3], [4, 5, 6]])

a.shape   # (2, 3) โ€” 2 rows, 3 columns
a.ndim    # 2 โ€” number of dimensions
a.dtype   # dtype('int64') โ€” the single data type every element shares
a.size    # 6 โ€” total number of elements
Every array has exactly one dtype for all its elements. Mixing an integer and a float in the same np.array([...]) call silently upcasts the whole array to float โ€” this is a common source of confusion for beginners who expect Python's mixed-type list behavior.

5. Indexing and slicing

a = np.array([10, 20, 30, 40, 50])

a[0]        # 10 โ€” first element
a[-1]       # 50 โ€” last element
a[1:4]      # array([20, 30, 40]) โ€” slice, same rules as Python lists
a[:3]       # array([10, 20, 30]) โ€” first three
a[::2]      # array([10, 30, 50]) โ€” every other element

# 2D indexing โ€” [row, column], not [row][column]
b = np.array([[1, 2, 3], [4, 5, 6]])
b[0, 2]     # 3 โ€” row 0, column 2
b[1, :]     # array([4, 5, 6]) โ€” entire second row
b[:, 0]     # array([1, 4]) โ€” entire first column
NumPy's comma-based [row, column] indexing is different from a nested Python list's [row][column] syntax. Both often work, but [row, column] is the idiomatic NumPy form and is required once you get into more advanced (boolean/fancy) indexing in the intermediate track.

6. Basic array math

a = np.array([1, 2, 3, 4])
b = np.array([10, 20, 30, 40])

a + b        # array([11, 22, 33, 44]) โ€” elementwise addition
a * b        # array([10, 40, 90, 160]) โ€” elementwise multiplication
a ** 2       # array([1, 4, 9, 16]) โ€” elementwise power
b / a        # array([10., 10., 10., 10.]) โ€” elementwise division

# Aggregate functions โ€” reduce the whole array to one number
a.sum()      # 10
a.mean()     # 2.5
a.max()      # 4
a.min()      # 1
a.std()      # standard deviation

All of the arithmetic operators above work elementwise by default โ€” this is different from how * behaves on two Python lists (which either errors or repeats the list, depending on the types involved). Elementwise-by-default is another core NumPy convention worth internalizing early.

7. Lab: a small analysis script

Beginner capstone lab

~25 min ยท Pure NumPy, no external data needed
  1. Create an array of 10 fictional daily temperatures (any reasonable values, in Celsius) using np.array().
  2. Convert the whole array to Fahrenheit in one vectorized line: fahrenheit = celsius * 9/5 + 32.
  3. Print the mean, max, and min of the Fahrenheit array using the aggregate methods from section 6.
  4. Slice out just the last 3 days and print them.
  5. Use boolean comparison to find which days were above the mean: celsius[celsius > celsius.mean()] โ€” print the result and note that this already previews the "boolean indexing" topic from the intermediate track.

8. Pre-attempt checklist

  • I can explain why NumPy arrays are faster than Python lists for numeric work
  • I can create an array from a list, and with zeros/ones/arange/linspace
  • I can check an array's shape, dtype, and ndim
  • I can index and slice both 1D and 2D arrays using [row, column] syntax
  • I can perform elementwise arithmetic and basic aggregates (sum, mean, max, min)

9. What to do after this lab

Rebuild the capstone script for the same problem using plain Python lists and a for loop instead of NumPy, and time both versions on a much larger array (say, 1,000,000 elements) using Python's built-in time module โ€” seeing the actual speed gap firsthand is more convincing than reading about it. Once array creation, indexing, and basic math feel automatic, move to the Intermediate track, which covers broadcasting, boolean/fancy indexing, and reshaping.

โ† Back to all practice tools & labs

Intermediate

Last updated: August 22, 2026
NumPy โ€” Intermediate Lab Instructions | CRMNuggets
Practice Tools & Labs โ€บ NumPy โ€บ Intermediate
๐ŸŸ  Intermediate

NumPy โ€” Intermediate Track

Where simple elementwise math stops being enough. This guide covers broadcasting, boolean and fancy indexing, reshaping, axis-aware aggregation, and basic linear algebra.

Completed the Beginner track Comfortable with array creation & slicing Python 3.9+, NumPy installed 60โ€“75 min lab

1. What changes at this level

Beginner work does math between arrays of the exact same shape. Intermediate work is about NumPy's rules for combining arrays of different shapes (broadcasting), selecting elements based on a condition rather than a fixed position (boolean/fancy indexing), and reasoning about multi-dimensional arrays along a specific axis rather than just as one flat aggregate. This is also where basic linear algebra โ€” the actual mathematical purpose NumPy was originally built for โ€” enters the picture.

2. Broadcasting

Broadcasting is the set of rules NumPy uses to perform elementwise operations on arrays of different shapes without you having to manually resize either one. The core rule: comparing shapes from the rightmost dimension inward, two dimensions are compatible if they're equal, or if one of them is 1.

import numpy as np

# Scalar broadcast โ€” this is broadcasting you already used in the beginner track
a = np.array([1, 2, 3])
a * 2                      # array([2, 4, 6]) โ€” the scalar "stretches" to match

# 1D array broadcast against each row of a 2D array
grid = np.array([[1, 2, 3], [4, 5, 6]])
row = np.array([10, 20, 30])
grid + row
# array([[11, 22, 33],
#        [14, 25, 36]])   โ€” row is applied to every row of grid

# Column vector broadcast against each column
col = np.array([[100], [200]])   # shape (2, 1)
grid + col
# array([[101, 102, 103],
#        [204, 205, 206]])
A shape mismatch that broadcasting can't resolve raises a ValueError: operands could not be broadcast together. When that happens, print both arrays' .shape first โ€” the fix is almost always reshaping one array (see section 4) so its dimensions align with the rule above, often using reshape(-1, 1) to turn a flat array into an explicit column vector.

3. Boolean and fancy indexing

Beginner indexing selects elements by fixed position. Boolean indexing selects elements by condition, and fancy indexing selects them by an arbitrary list of positions โ€” both return new arrays rather than a single value.

a = np.array([15, 22, 8, 40, 3, 27])

# Boolean indexing โ€” the condition builds a True/False array of the same shape,
# then that mask selects only the True positions
mask = a > 20
mask          # array([False, True, False, True, False, True])
a[mask]       # array([22, 40, 27])
a[a > 20]     # same thing, written inline โ€” the common idiom

# Modify in place using a boolean mask
a[a < 10] = 0
a             # array([15, 22, 0, 40, 0, 27])

# Fancy indexing โ€” select by an explicit list of positions, in any order
idx = [0, 3, 5]
a[idx]        # array([15, 40, 27])

# Combine conditions with & and | (not "and"/"or" โ€” those don't work elementwise)
a[(a > 10) & (a < 30)]
Python's and/or keywords only work on single boolean values, not arrays โ€” using them on a NumPy condition raises ValueError: truth value of an array is ambiguous. Always use &/| with parentheses around each condition when combining array comparisons.

4. Reshaping arrays

a = np.arange(12)          # array([0, 1, 2, ..., 11]), shape (12,)

a.reshape(3, 4)             # 3 rows, 4 columns โ€” total elements must match (3*4=12)
a.reshape(4, 3)              # 4 rows, 3 columns โ€” same data, different layout
a.reshape(-1, 1)             # -1 means "figure this dimension out automatically"
                              # โ†’ turns a flat array into an explicit column vector

grid = a.reshape(3, 4)
grid.flatten()               # back to 1D, as a copy
grid.T                       # transpose โ€” swaps rows and columns, shape becomes (4, 3)
reshape and flatten don't change the underlying data โ€” they change how NumPy interprets the same block of memory. This is why reshaping a huge array is essentially free in terms of performance, unlike actually copying or rebuilding it.

5. Axis-aware aggregation

Aggregate functions like sum() and mean() default to collapsing the entire array to one number. On a multi-dimensional array, the axis parameter lets you collapse along just one dimension instead.

grid = np.array([[1, 2, 3], [4, 5, 6]])   # shape (2, 3)

grid.sum()             # 21 โ€” every element added together
grid.sum(axis=0)       # array([5, 7, 9]) โ€” sum DOWN each column (collapses rows)
grid.sum(axis=1)       # array([6, 15])   โ€” sum ACROSS each row (collapses columns)

grid.mean(axis=0)      # column-wise means
grid.max(axis=1)       # row-wise maximums
The rule that trips almost everyone up at first: axis=0 operates down columns (collapsing the row dimension), and axis=1 operates across rows (collapsing the column dimension) โ€” it names the axis being collapsed, not the direction you're moving along. Test a small example like the one above whenever you're unsure, rather than guessing.

6. Basic linear algebra

a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])

a @ b                # matrix multiplication (preferred modern syntax)
np.dot(a, b)         # equivalent, older syntax
a * b                # NOT matrix multiplication โ€” this is still elementwise!

np.linalg.inv(a)     # matrix inverse
np.linalg.det(a)     # determinant
np.linalg.eig(a)     # eigenvalues and eigenvectors, returned as a tuple

# Solving a system of linear equations Ax = b
A = np.array([[3, 1], [1, 2]])
b_vec = np.array([9, 8])
np.linalg.solve(A, b_vec)   # the x that satisfies the system
The single most common intermediate-level bug: using * when matrix multiplication was intended. * is always elementwise on NumPy arrays โ€” for true matrix multiplication, use @ (or np.dot). Both are valid NumPy operations; they just answer completely different mathematical questions.

7. Lab: a small grade-book analysis

Intermediate capstone lab

~40 min ยท Pure NumPy, no external data needed
  1. Build a 2D array of shape (5 students, 4 test scores) with made-up integer scores between 40 and 100.
  2. Compute each student's average score using axis=1, and each test's class average using axis=0.
  3. Use boolean indexing to find all individual scores below 60 ("at risk"), and count them with .sum() on the boolean mask (True counts as 1).
  4. Broadcast a 1-per-test "weight" array (shape (4,), e.g. [0.2, 0.2, 0.3, 0.3]) against the scores and sum along axis=1 to compute a weighted final grade per student.
  5. Reshape the weighted-grade result into an explicit column vector with reshape(-1, 1) and print it alongside the original grid using np.hstack (or just print both separately if you haven't covered stacking functions yet).

8. Pre-attempt checklist

  • I can explain NumPy's broadcasting rule and predict whether two shapes are compatible
  • I can build and apply a boolean mask, and combine conditions with &/|
  • I can reshape an array and explain why it doesn't copy the underlying data
  • I can correctly choose axis=0 vs. axis=1 for a given aggregation question
  • I know the difference between * (elementwise) and @ (matrix multiplication)
Once broadcasting, boolean indexing, and axis-aware aggregation feel automatic, move to the Expert track, which covers performance/vectorization patterns, memory layout and views vs. copies, structured arrays, and interfacing with C/Cython.

โ† Back to all practice tools & labs

Expert

Last updated: August 22, 2026
NumPy โ€” Expert Lab Instructions | CRMNuggets
Practice Tools & Labs โ€บ NumPy โ€บ Expert
๐Ÿ”ด Expert

NumPy โ€” Expert Track

Views vs. copies, memory layout and strides, real vectorization performance work, structured arrays, and interfacing with C/Cython for the cases NumPy alone can't make fast enough.

Completed broadcasting & boolean indexing Comfortable with basic C concepts helpful NumPy, and optionally Cython, installed 90+ min lab

1. What changes at this level

Intermediate work treats NumPy as a black box that's simply "faster than loops." Expert work is about understanding why โ€” how arrays are actually laid out in memory, when an operation returns a view (sharing memory with the original) versus a copy, how to profile and eliminate the last hidden Python-level loops in your code, and when NumPy itself isn't enough and you need to drop into compiled code directly. This track assumes real fluency with everything in the intermediate guide.

2. Views vs. copies

This is the single most consequential thing to understand at the expert level, because getting it wrong causes silent bugs rather than errors. Basic slicing returns a view โ€” a new array object that shares the same underlying memory as the original. Boolean indexing, fancy indexing, and most operations that reshape or combine data return a copy.

import numpy as np

a = np.arange(10)
b = a[2:5]          # a VIEW โ€” shares memory with a
b[0] = 999
a                    # array([0, 1, 999, 3, 4, 5, 6, 7, 8, 9]) โ€” a changed too!

c = a[[2, 3, 4]]     # fancy indexing โ€” a COPY
c[0] = -1
a                    # unchanged by modifying c

# Check explicitly rather than guessing
b.base is a          # True โ€” b is a view into a
c.base is a           # False โ€” c owns its own data

# Force a copy when you need one, regardless of the default behavior
d = a[2:5].copy()
A function that takes an array, slices it, and mutates the slice in place will silently mutate the caller's original array if the slice was a view โ€” this is a very common source of hard-to-trace bugs in larger codebases. When writing a function you intend to be pure (non-mutating), call .copy() explicitly rather than relying on the caller to know which operations return views.

3. Memory layout: strides and contiguity

A NumPy array's shape is really just a "view configuration" on top of a flat block of memory, and strides โ€” the number of bytes to step in memory to move one position along each axis โ€” are what let the same underlying data be reinterpreted as different shapes without copying.

a = np.arange(12).reshape(3, 4)
a.strides            # (32, 8) on a typical 64-bit int64 array โ€”
                      # 32 bytes to move one row down, 8 bytes to move one column right

a.flags['C_CONTIGUOUS']   # True โ€” stored row-major (C order), the NumPy default
a.T.flags['C_CONTIGUOUS'] # False โ€” the transpose is a view with different strides,
                           # not physically reordered data

# Forcing a specific memory layout matters for performance-critical code,
# especially when passing arrays to C extensions or certain BLAS-backed operations
b = np.ascontiguousarray(a.T)
Operations on a non-contiguous array (like an unmaterialized transpose) are often noticeably slower than the same operation on a contiguous one, because the CPU can't stream through memory linearly. If you're chasing a performance issue and the shapes and dtypes all look right, checking .flags['C_CONTIGUOUS'] is a reasonable next diagnostic step.

4. Real vectorization: eliminating hidden Python loops

The intermediate track taught you to prefer vectorized operations over explicit loops. Expert work is about finding the loops that are still hiding โ€” often inside a seemingly-vectorized line that's secretly falling back to slow, general-purpose code.

# Slow: np.vectorize is just a thin wrapper around a Python loop โ€”
# it exists for convenience/broadcasting semantics, NOT for speed
def my_func(x):
    return x ** 2 if x > 0 else 0
slow = np.vectorize(my_func)(a)

# Fast: express the same logic using real vectorized primitives
fast = np.where(a > 0, a ** 2, 0)

# Slow: appending to a Python list inside a loop, converting to array at the end,
# is often fine for small N but breaks down badly at scale
results = []
for row in big_array:
    results.append(row.sum())
results = np.array(results)

# Fast: axis-aware aggregation does the same thing with zero Python-level looping
results = big_array.sum(axis=1)

Profiling before optimizing

# %timeit inside IPython/Jupyter โ€” the standard first step
%timeit my_slow_function(data)

# From a plain script, use the timeit module directly
import timeit
timeit.timeit(lambda: my_slow_function(data), number=100)
Don't guess at which line is slow โ€” measure. A common expert-level mistake is spending an hour hand-optimizing a line that accounts for 2% of total runtime while an unnoticed np.vectorize call elsewhere accounts for 90% of it.

5. Structured arrays

A structured array lets a single NumPy array hold named fields of different types per "row," similar in spirit to a lightweight, fixed-schema table โ€” useful when you want array-level performance without pulling in a full library like pandas.

dt = np.dtype([('name', 'U10'), ('age', 'i4'), ('score', 'f8')])
people = np.array([('Ada', 32, 91.5), ('Grace', 45, 88.2)], dtype=dt)

people['name']         # array(['Ada', 'Grace'], dtype=' 90]   # rows matching a condition, same boolean-mask idiom as before

6. Interfacing with C and Cython

For the small fraction of cases where even well-vectorized NumPy code isn't fast enough (typically tight numerical loops with data-dependent branching that can't be expressed as array operations at all), the standard escape hatches are:

  • Cython โ€” write Python-like code with optional static type declarations, compiled to C. The most common on-ramp for NumPy-heavy code because it understands NumPy arrays natively via typed memoryviews.
  • Numba โ€” a JIT compiler that speeds up plain Python/NumPy functions with a single @njit decorator, no separate compilation step required. Often the lowest-effort option to try first.
  • The NumPy C API directly โ€” writing a C extension that manipulates ndarray objects at the buffer level; the most control, and the most work, reserved for library-authoring rather than typical application code.
# Numba example โ€” often the fastest path from "slow Python loop" to "fast compiled loop"
from numba import njit

@njit
def sum_of_squares(arr):
    total = 0.0
    for x in arr:
        total += x ** 2
    return total
Reach for Numba or Cython only after profiling has shown that a genuinely loop-heavy, hard-to-vectorize section is the actual bottleneck โ€” most code that "needs Cython" turns out to just need a better-chosen NumPy vectorized expression instead.

7. Lab: profile, diagnose, and fix a slow pipeline

Expert capstone lab

90+ min ยท Requires IPython/Jupyter for %timeit
  1. Write a deliberately slow function that computes, for a large 1D array (~1,000,000 elements), a new array where each element is squared if positive and zeroed if not โ€” implemented using np.vectorize.
  2. Profile it with %timeit and record the result.
  3. Rewrite it using np.where, profile again, and record the speedup.
  4. Create a large 2D array, take a transposed view of it, and compare .flags['C_CONTIGUOUS'] before and after; time a .sum() on both the original and the transposed view to observe any layout-driven performance difference.
  5. Deliberately create a view-vs-copy bug (mutate a slice of an array inside a function and observe the caller's array change), then fix it using .copy().
  6. If Numba is installed, reimplement the original slow function with @njit and compare its timing against both earlier versions.

8. Pre-attempt checklist

  • I can predict whether a given NumPy operation returns a view or a copy
  • I can check and explain an array's strides and C-contiguity
  • I can identify a hidden Python-level loop (e.g. np.vectorize) and replace it with a true vectorized equivalent
  • I profile before optimizing, using %timeit or timeit, rather than guessing
  • I can build a structured array and explain when it's a reasonable alternative to pandas
  • I know the standard escape hatches (Numba, Cython, the C API) and when each is appropriate
This is the last of the three standard tracks. From here, reading NumPy's own internals documentation and the source of a well-optimized library built on top of it (pandas' or scikit-learn's array-handling code, for example) is the most direct way to keep building expert-level intuition โ€” most of what separates expert NumPy usage from intermediate usage is pattern recognition earned by reading other people's fast code.

โ† Back to all practice tools & labs