One type, laid out end to end - and why almost everything else about the library follows from that.
Overview
Two ways to hold five numbers
NumPy exists because Python lists are too slow for numerical work. The difference is not a small percentage. It is often a factor of a hundred. That gap comes from how each structure stores data in memory.
A Python list stores pointers. Each element is a Python object sitting somewhere in memory, with its own type information, reference count, and value. Adding two lists elementwise means following each pointer, checking each type, calling the addition operator, and building a new object for the result. That is a lot of work per number.
A NumPy array stores raw values. Five int64 numbers are forty bytes in a row, with the type recorded once for the whole array. Adding two arrays is a single compiled loop over those bytes. No pointer chasing, no type checking, no object creation. The loop runs in C, not in Python.
That speed difference is the entire reason NumPy exists. Every library that does numerical work in Python -- pandas, scikit-learn, scipy, matplotlib -- builds on NumPy arrays underneath. Learning NumPy means learning the data structure that the whole ecosystem shares.
The cost of that speed is rigidity. Every element has the same type. The array has a fixed size. Appending is expensive. These constraints are what make the compiled loops possible. If every element could be a different type, the code would need the same per-element checks that make lists slow.
This module introduces the array, shows how it differs from a list in memory and in speed, and explains the one-dtype constraint that everything else follows from.
A Python list of five ints is five pointers, each to a full PyObject sitting one spot else in memory. Each of those objects carries a type, a link count and the value. Adding two lists elementwise means following each pointer, checking each type. Building a new object for each result.
A NumPy array of five ints is forty bytes. Five 64-bit numbers, one after another, with the type recorded once for the whole array.
That single gap shows nearly all else in this track. It's why arrays are fast, why they've a dtype, why they can't hold mixed types, why slicing gives you a view instead of a copy. Why shapes matter so much.
Worth knowing
A list holds pointers to objects; an array holds the numbers themselves, of one dtype, contiguous in memory.
Operations apply to the whole array in compiled code. You describe the operation once rather than writing the loop.
+ concatenates lists and adds arrays elementwise. That difference catches people converting code.
An array has one dtype. Mixing types promotes the whole array — add a string and every element becomes text.
The default integer dtype is platform-dependent: int64 on most desktops, int32 on this page's 32-bit WebAssembly. Ask for a width explicitly whenever the values are large.
Shape is as important as contents. Most of NumPy is rules about what shapes do when combined.
These pages run CPython on WebAssembly, so timings are several times slower than native. Ratios transfer; seconds do not.
What NumPy Is For
One type, laid out end to end. Why almost all else follows from that.
A list of numbers is not a block of numbers
A Python list holds pointers to objects scattered through memory. An array holds the numbers themselves, one type, end to end.
example_01.pyNumPy
import numpy as np
import sys
nums = [1, 2, 3, 4, 5]
arr = np.array([1, 2, 3, 4, 5])
print("list :", nums, type(nums).__name__)
print("array:", arr, type(arr).__name__)
print()
print("every list element is a full Python object:")
print(" one int :", sys.getsizeof(1), "bytes")
print(" the list :", sys.getsizeof(nums), "bytes of pointers alone")
print()
print("the array stores the numbers themselves:")
print(" dtype :", arr.dtype)
print(" itemsize :", arr.itemsize, "bytes each")
print(" nbytes :", arr.nbytes, "bytes total")
Output
Arithmetic happens to the whole array
No loop, no comprehension. The operation is described once and applied to every element by compiled code.
example_02.pyNumPy
import numpy as np
nums = [1, 2, 3, 4, 5]
arr = np.array([1, 2, 3, 4, 5])
# Python: you write the loop
doubled_list = [n * 2 for n in nums]
# NumPy: you write the operation
doubled_arr = arr * 2
print("list :", doubled_list)
print("array :", doubled_arr)
print()
print("and + means something different for each:")
print(" list + list :", nums + nums, "<- concatenation")
print(" array + array :", arr + arr, "<- elementwise addition")
Output
One dtype, decided up front
An array has a single type for every element. Mixing types does not make a mixed array — it promotes the whole thing.
example_03.pyNumPy
import numpy as np
# NOTE: the default integer is platform-dependent. Most desktops give
# int64; this page runs 32-bit WebAssembly, so it reports int32.
print("ints :", np.array([1, 2, 3]).dtype, "(platform default)")
print("floats :", np.array([1.0, 2.0]).dtype)
print("one float :", np.array([1, 2, 3.0]).dtype, "<- promoted the whole array")
print("with a str :", np.array([1, 2, "three"]).dtype, "<- everything became text")
print("booleans :", np.array([True, False]).dtype)
print()
a = np.array([1, 2, 3])
print("assigning a float into an int array:")
a[0] = 9.7
print(" ", a, "<- truncated, because the dtype cannot change")
Output
How much faster, measured here
The gap is real but this page runs on WebAssembly, so read the ratio and not the seconds.
example_04.pyNumPy
import numpy as np
import time
n = 200_000
nums = list(range(n))
# int64 explicitly: this page is 32-bit WebAssembly, where NumPy's default
# integer is int32, and squaring values this large would silently overflow.
arr = np.arange(n, dtype=np.int64)
t = time.perf_counter()
total_list = sum(x * x for x in nums)
py = time.perf_counter() - t
t = time.perf_counter()
total_arr = int((arr * arr).sum())
npy = time.perf_counter() - t
print("python loop : %.4f s" % py)
print("numpy : %.4f s" % npy)
print("ratio : %.1fx" % (py / npy))
print("same answer :", total_list == total_arr)
Output
Shape is the other half
An array is not just a list of numbers — it has a shape, and most of NumPy is about what shapes do together.
example_05.pyNumPy
import numpy as np
a = np.array([1, 2, 3, 4, 5, 6])
b = a.reshape(2, 3)
print("flat :", a, "shape", a.shape, "ndim", a.ndim)
print()
print("as 2x3 :")
print(b)
print(" shape:", b.shape, "ndim", b.ndim, "size", b.size)
print()
print("same numbers, same memory - only the shape changed:")
print(" shares memory:", np.shares_memory(a, b))
Output
Where it sits in the stack
pandas, scikit-learn, SciPy and every deep learning framework hold NumPy arrays or something modelled on them.
example_06.pyNumPy
import numpy as np
# A tiny linear model, written the way every library underneath does it.
X = np.array([[1.0, 2.0],
[1.0, 3.0],
[1.0, 4.0]]) # a bias column and one feature
w = np.array([0.5, 2.0]) # parameters
y_pred = X @ w # one matrix multiply, not a loop
y_true = np.array([4.0, 6.5, 8.0])
print("predictions :", y_pred)
print("residuals :", y_true - y_pred)
print("mean sq err :", float(((y_true - y_pred) ** 2).mean()))
print()
print("This is the shape of every fit, forward pass and score you will meet.")
Output
Vectorisation
Because the numbers are contiguous and all the same type, the loop can happen in compiled code:
doubled = arr * 2
There's no Python-level loop. NumPy hands the whole block to a C routine that walks it. Modern CPUs process a few elements per instruction while it does.
The mental shift is from *how do I loop over this* to *what step am I applying*. That reads oddly at first and turns the natural way to think within about a week.
+ means two different things
Worth stating early because it's the first thing that surprises people converting code:
Lists concatenate. Arrays add. Neither is wrong. They're other types with other meanings for the same symbol. Code that assumes one while holding the other makes a thing plausible rather than an error.
One dtype, and it is decided for you
each element of an array has the same type. NumPy picks the narrowest type that fits all you gave it.
[1, 2, 3] turns the platform's default int. On most desktop machines that is int64. On this page. This Runs 32-bit WebAssembly, it's int32. That gap isn't cosmetic. An int32 overflows at about 2.1 billion, silently and without a warning. And it's the reason examples here that times large ints ask for dtype=np.int64 by name. Add one float and the whole array turns float64. Not a mixture. Add a string and each element turns a fixed-width string, including the numbers.
That last case is worth watching for. It makes an array that still looks fair when printed and does nothing you want arithmetically.
There's a result people meet early: assigning into an array can't change its dtype. Put 9.7 into an int64 array and it truncates to 9, silently. The array's type was fixed when it was created.
Speed, honestly
The fourth editor above measures it. Expect a large ratio. Read the ratio rather than the seconds.
These pages run CPython compiled to WebAssembly. This Is a few times slower than a native interpreter for the Python parts. That fine falls mostly on the Python loop. If anything the gap here flatters NumPy. The direction is right and the magnitude is roughly right. The abs numbers don't transfer to your laptop.
The speed also has limits worth knowing now. Arrays are fast for whole-array steps on numeric data. They aren't fast if you loop over them in Python one element at a time. That is slower than a list. Each element has to be boxed into a Python object on the way out.
Shape
An array carries a shape and contents. Most of the notable action in this library is about what shapes do when mixed.
a.shape is a tuple. (6,) is one-dimensional with six elements. (2, 3) is two rows of three. (2, 3, 4) is two blocks of three rows of four.
Reshaping is often free, because it changes how the same block of memory is read rather than moving anything. The last editor shows np.shares_memory confirming that the reshaped array is the same numbers.
Where this sits
Almost nothing in scientific Python reimplements arrays. pandas holds NumPy arrays under its columns. scikit-learn takes and returns them. SciPy builds on them. PyTorch and TensorFlow use their own tensor types on purpose modelled on the same interface. That @, broadcasting and .shape mean what you already expect.
Learning NumPy properly is so not one library. It's the vocab that the rest of the stack assumes you've.
What this track covers
The array itself first: creating one, its dtype, its shape. What indexing gives you. Then operating on arrays: elementwise math, broadcasting, boolean masks, aggregation along an axis. Then structure: views versus copies, stacking, transposing, sorting. Then the numeric end: random numbers, linear algebra, missing data. The speed rules that follow from the memory layout.
each idea is a small program on the page. Change the numbers and press Run. A shape rule you've watched break is one you'll note.
Why the list is slow, concretely
It's worth being precise about where the time goes. "Python is slow" isn't an reason and doesn't tell you what to change.
A Python list of five ints is an array of five pointers. Each pointer leads one spot else in memory, to a PyObject that carries a link count, a type pointer. Only then the actual value. A small int occupies twenty-eight bytes and sits wherever the allocator put it.
Adding two lists elementwise so means: follow a pointer, check the type, extract the value, follow another pointer, check that type, extract that value, perform the addition, allocate a new object for the result. Store a pointer to it. Ten times over for ten elements.
The NumPy array is five ints, adjacent, with nothing between them. The addition is a loop in compiled C over contiguous memory, with the type checked once for the whole array rather than once per element.
Two split wins come out of that. The obvious one is skipping the interpreter. The less obvious one is nearness. The processor fetches memory in blocks. Reading in a row values costs a fraction of what chasing scattered pointers costs. On large arrays the second effect can matter as much as the first.
This also shows the cases where NumPy doesn't help. If your data is a thousand elements and you touch it once, the interpreter cost you avoided was small and the conversion cost wasn't. The wins scale with array size.
What NumPy deliberately does not do
Knowing the boundaries saves a lot of fighting with it.
It doesn't grow. There's no fast append. The size is fixed at creation. Each function that appears to extend an array is allocating a new one and copying. This isn't an oversight. It's the price of the contiguous block that makes all else fast.
It doesn't mix types. One dtype per array. A column of names and a column of ages are two arrays, or a structured dtype, or a pandas DataFrame. Not one NumPy array of mixed values.
It handles strings poorly. NumPy strings are fixed width. Assigning a longer value silently truncates it. Object arrays hold real Python strings but give up each speed gain. They're back to storing pointers.
It has no concept of missing data. Floats can carry NaN. Ints can't carry anything. There's no null.
When to use something else
Small data touched once. A hundred values processed in a script: a list is simpler and the gap is unmeasurable.
Heterogeneous records with labels. That is pandas. It's built on NumPy. You lose nothing. You gain named columns, mixed types per column and real handling of missing values.
Nested or ragged structure. Lists of other lengths don't form an array. Forcing them into an object array gets you the syntax without the speed.
Data larger than memory. NumPy assumes the array fits. Dask, Zarr and HDF5 exist for when it doesn't.
The model to carry forward
all else in this track is a result of one idea. An array is a flat block of same values, plus a small description of how to read it. The shape, the dtype. The number of bytes to step for each axis.
Reshaping changes the description and not the block. Transposing changes the description. Slicing changes the description. All of them are free.
Masking and fancy indexing can't be given that way, so they copy.
Math runs over the block in compiled code. Broadcasting is a rule for pretending two descriptions are matching without changing either block.
Hold onto that and most of NumPy's action stops being a set of rules to store and turns a thing you can predict.
Questions people ask at this point
Does NumPy replace Python lists?
No. Lists are the right structure for heterogeneous, growing sets of any objects. They remain the right answer for most plain Python. NumPy is for a exact shape of problem. Many values of the same type, operated on together. Reaching for an array to hold three config values is worse than a list, not better.
Do I need to install it?
NumPy isn't in the standard library. It's pip install numpy. It's a dependency of nearly all numeric. It's often already present in a scientific setting. It ships as a compiled wheel for each common platform. So Installation doesn't need a compiler.
**Why is it always imported as np?**
rule, and a strong one. Nearly all published code, docs and examples use import numpy as np. Following it makes your code readable to anyone who has seen NumPy before. There's no technical reason, and no good reason to deviate.
Is NumPy still relevant with pandas and PyTorch around?
It's underneath both. A pandas Series wraps a NumPy array. PyTorch tensors follow the same shape and broadcasting rules and convert to arrays in one call. all in this track transfers. This Is a large part of why it's worth learning properly rather than by copying snippets.
How much of it do I need?
Less than the docs suggests. NumPy has hundreds of functions and a working knowledge of maybe thirty covers most real code. The concepts. Shape, dtype, views, broadcasting. Matter far more than the function list. They let you predict action instead of looking it up.
How to work through this track
Each module has runnable editors above the text. They aren't decoration. The fastest way to build an accurate mental model of shapes and dtypes is to change a number, run it, and see what happens.
The parts worth slowing down on are the ones where NumPy's action differs from Python's. Int overflow, views sharing memory, broadcasting shapes you didn't intend. NaN. Those four account for most of the surprises in real code, and each has a module.
The parts you can skim on a first pass are the function inventories. The exact names in stacking, sorting and set steps. Knowing that a function exists is enough. The signature is a search away.
Check yourself
0 of 4
Answer without scrolling back up.
What is the fundamental difference between a list and an array?
Everything else - speed, dtype, views, broadcasting - follows from that memory layout.
What does `np.array([1, 2, "three"])` produce?
An array has one dtype, so everything is promoted to the type that fits all of it - here, text. It prints plausibly and does nothing useful arithmetically.
You assign `a[0] = 9.7` into an int64 array. What happens?
The dtype was fixed when the array was created and assignment cannot change it, so the value is truncated silently.
Why are the timings on these pages not directly transferable?
The penalty falls mostly on the Python loop, so the ratio is roughly right and if anything flatters NumPy. Read the ratio, not the seconds.
Cheat sheet
What NumPy Is For
See why NumPy stores numbers in one contiguous block of typed memory and how that layout makes array operations orders of magnitude faster.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.