On this page
This NumPy tutorial starts with the array: a fixed-type, contiguous block of memory that makes numerical Python fast. Doubling a million integers ran roughly 20 times faster as an array than as a list in the timing below. pandas, scikit-learn, PyTorch and matplotlib all build on it.
You can use those libraries without knowing NumPy, right up until an error mentions a shape or a dtype. One stray string in a column of numbers, for instance, silently turns the whole array into text. Part one is for Python beginners moving into data work: what arrays are, why they are fast, how to create them and how to avoid the dtype trap. Part two moves on to indexing and slicing. If you have not set up Python yet, follow the Anaconda on Windows guide or the macOS version first.
As of September 2026 the latest release is NumPy 2.5. The outputs in this NumPy tutorial come from NumPy 2.2 and match the behaviour described in the current NumPy 2.x documentation.
Why is a NumPy array faster than a Python list?
A Python list is a sequence of pointers. Each element is a full Python object stored somewhere in memory, and the list holds their addresses.
By contrast, a NumPy array is a single contiguous block of memory holding raw values of one fixed type.
That difference has two consequences. The array uses far less memory, and operations on it run as compiled loops over adjacent memory rather than as interpreted Python.
import numpy as np
import time
py_list = list(range(1_000_000))
np_array = np.arange(1_000_000)
start = time.perf_counter()
result = [x * 2 for x in py_list]
print(f"list: {time.perf_counter() - start:.4f}s")
start = time.perf_counter()
result = np_array * 2
print(f"array: {time.perf_counter() - start:.4f}s")
Timed properly with timeit on NumPy 2.2, the array version was roughly 20 times faster on a million integers. The exact ratio depends on your hardware and the operation, but it grows with the size of the data.
Worked example: the memory difference
import sys
print(np_array.nbytes) # 8000000 - 8 bytes per int64
print(sys.getsizeof(py_list)) # 8000056 - the pointers alone
print(sys.getsizeof(1)) # 28 - each int object on top of that
The list's 8 MB covers only the pointers. Each integer it points to is a separate object of about 28 bytes on CPython, so the real total is several times the array's 8 MB.
How do you create NumPy arrays?
From an existing sequence:
import numpy as np
a = np.array([1, 2, 3, 4])
print(a) # [1 2 3 4]
print(a.dtype) # int64
b = np.array([[1, 2, 3],
[4, 5, 6]])
print(b.shape) # (2, 3)
print(b.ndim) # 2
print(b.size) # 6
The default integer is int64 on every 64-bit platform, including Windows, since NumPy 2.0. Older tutorials that show int32 on Windows describe NumPy 1.x.
From scratch, which is what you will use most:
np.zeros((2, 3)) # 2x3 of 0.0
np.ones(5) # [1. 1. 1. 1. 1.]
np.full((2, 2), 7) # 2x2 of 7
np.eye(3) # 3x3 identity
np.empty(3) # uninitialised - contains whatever was in memory
np.arange(0, 10, 2) # [0 2 4 6 8] - like range()
np.linspace(0, 1, 5) # [0. 0.25 0.5 0.75 1. ] - n evenly spaced points
arange takes a step, whereas linspace takes a count. The arange documentation itself recommends linspace for non-integer steps, because floating-point steps can overshoot:
np.arange(1, 1.3, 0.1) # [1. 1.1 1.2 1.3] - four values, the stop leaked in
np.linspace(1, 1.2, 3) # [1. 1.1 1.2] - exactly three, as asked
For random data, use the generator API:
rng = np.random.default_rng(seed=42)
rng.random(3) # [0.77395605 0.43887844 0.85859792]
rng.integers(0, 10, size=5) # [0 6 2 0 5]
rng.normal(loc=0, scale=1, size=4) # four draws from a standard normal
Always seed when you want reproducible results. The older np.random.rand style still works, but default_rng is the recommended constructor in current NumPy.
What is the dtype upcasting trap?
Every array has exactly one dtype. Put mixed types in and NumPy silently promotes to whatever holds them all:
np.array([1, 2, 3]).dtype # int64
np.array([1, 2, 3.0]).dtype # float64 - one float promotes everything
np.array([1, 'a']).dtype # <U21 - everything becomes a string
That last one is the bug you will actually hit. A single stray string in a column of numbers turns the whole array into text. Arithmetic then fails with a confusing UFuncTypeError about a missing loop for dtype('<U21').
Set the dtype explicitly when it matters:
arr = np.array([1.9, 2.5, 3.1])
np.array([1, 2, 3], dtype=np.float64)
np.array([1, 0, 1], dtype=bool) # [ True False True]
arr.astype(np.int32) # [1 2 3] - convert to a new array
Be aware that astype truncates rather than rounds when going from float to int: np.array([1.9]).astype(int) gives [1], not [2]. Also note that the old aliases np.float and np.int no longer exist, so use float, int or an explicit type such as np.float64.
What are vectorised operations in NumPy?
Vectorised operations apply to the whole array at once, with no Python loop. This is the mental shift that matters:
a = np.array([1, 2, 3, 4])
a + 10 # [11 12 13 14]
a * 2 # [2 4 6 8]
a ** 2 # [ 1 4 9 16]
a > 2 # [False False True True]
b = np.array([10, 20, 30, 40])
a + b # [11 22 33 44] - elementwise
a * b # [ 10 40 90 160]
Note that * is elementwise multiplication, not matrix multiplication. For the matrix product, use @ or np.matmul, which Part 4 of this series covers alongside the rest of the linear algebra.
The rule of thumb: if you are writing a Python for loop over a NumPy array, there is almost always a vectorised way that is both faster and shorter.
arr = np.array([1, 2, 3])
# don't
result = []
for x in arr:
result.append(x * 2 + 1)
# do
result = arr * 2 + 1 # [3 5 7]
How do you check an array's shape and dtype?
Read the .shape, .ndim, .size and .dtype attributes. Those four answer most questions:
arr = np.arange(12).reshape(3, 4)
arr.shape # (3, 4) - length along each axis
arr.ndim # 2 - number of axes
arr.size # 12 - total elements
arr.dtype # int64 - element type
shape is the one you will check constantly. Most NumPy errors are shape mismatches, so reading the shape first turns a confusing traceback into an obvious problem.
One NumPy 2 detail surprises people in notebooks. If a cell ends with a single element, such as arr[0, 0], Jupyter shows np.int64(0) rather than 0. That is the new scalar representation, not a bug; print() still shows the plain number.
NumPy array quick reference
| Task | Function | Example result |
|---|---|---|
| Array from a list | np.array([1, 2, 3]) |
[1 2 3] |
| Filled array | np.zeros, np.ones, np.full |
np.full((2, 2), 7) gives a 2x2 of 7 |
| Integer range | np.arange(0, 10, 2) |
[0 2 4 6 8] |
| N evenly spaced points | np.linspace(0, 1, 5) |
[0. 0.25 0.5 0.75 1.] |
| Reproducible random numbers | np.random.default_rng(42) |
a seeded Generator |
| Change type | arr.astype(np.float64) |
a new array |
| Inspect | .shape, .ndim, .size, .dtype |
(3, 4), 2, 12, int64 |
Common mistakes with NumPy arrays
Most beginners hit the same handful of problems, and each one is covered earlier in this NumPy tutorial:
- Using
np.emptyas if it werenp.zeros. It skips initialisation, so the values are whatever was in memory. - Using
arangewith a float step. Uselinspaceinstead when you need an exact number of points. - Loading mixed data into one array. One text value turns everything into strings, so clean the data first.
- Expecting
astype(int)to round. It truncates; callnp.roundfirst if you want rounding. - Writing loops over arrays. Vectorise instead, because the loop throws away the speed you came for.
What comes next in this NumPy tutorial?
Part two covers indexing and slicing, including the critical detail that NumPy slices are views into the original array, not copies. That behaviour surprises nearly everyone the first time they modify one.
The Sunday Growth Brief
One email a week: the best new comparisons, a fresh roadmap and the tech news worth your attention.
No spam. Unsubscribe in one click.
Related reading
NumPy Part 2: array indexing and slicing continues the series, then Part 3 on broadcasting. For how Python's own number types behave, see Python numeric data types. For where NumPy sits in the wider stack, see tools and techniques for data science, and for a one-page summary the NumPy cheat sheet is the companion reference.
Frequently asked questions
Why use NumPy instead of Python lists?
There are two reasons. First, memory: an array stores raw values contiguously, whereas a list stores pointers to separate Python objects. Second, speed: array operations run as compiled loops over the whole block instead of interpreting Python bytecode per element. The gap is usually between ten and a hundred times, depending on the operation and the hardware.
What is a dtype in NumPy?
A dtype is the single data type shared by every element in an array, such as int64, float64 or bool. It is fixed when the array is created, which is what makes the memory layout predictable and the operations fast. If you need a different type, astype() creates a new array rather than changing the existing one in place.
Do I need NumPy if I already use pandas?
pandas is built on NumPy, so you are already using it whenever you use a DataFrame. You need enough NumPy to understand dtypes, shapes and broadcasting, because pandas error messages and performance advice refer to those concepts constantly. Learning the array basics first makes pandas behaviour far less mysterious.
What is the difference between an array's size and its shape?
Shape is a tuple giving the length along each dimension, for example (3, 4) for three rows and four columns. Size is the total number of elements, which is the product of the shape, so 12 in that example. The ndim attribute tells you how many dimensions there are, which is simply the length of the shape tuple.
Why does NumPy print np.int64(10) instead of 10?
Since NumPy 2.0, the repr of a NumPy scalar includes its type, so a notebook cell ending in a[0] shows np.int64(10). The value is still ten. print() and str() show the plain number, and you can convert explicitly with int() or float() when you need a built-in Python type.
Written by
Vanshika Nigam
ETL & Data Engineer, Accenture
ETL developer at Accenture working with Informatica, Snowflake and DBT, specialising in data mapping, cleansing and pipeline performance.

