Skip to content
1stepGrowLearnCompareGrow

NumPy Tutorial for Beginners: NumPy Arrays in Python (Part 1)

NumPy tutorial for beginners: what an array is, why it ran about 20 times faster than a list here, how one stray string turns numbers into text, and more.

Vanshika Nigam

ETL & Data Engineer, Accenture

9 min readUpdated
Share
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.

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

Python
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:

Python
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:

Python
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:

Python
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:

Python
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:

Python
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:

Python
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:

Python
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.

Python
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:

Python
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.empty as if it were np.zeros. It skips initialisation, so the values are whatever was in memory.
  • Using arange with a float step. Use linspace instead 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; call np.round first 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.

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.

SQLETL pipelinesSnowflakeData analysis

Related reading

Free weekly newsletter

Get the shortlist before everyone else

Every Sunday we send one email with the week’s sharpest course comparison, a career roadmap worth stealing, and the tech news and hiring signals we’re watching.

  • No fluff, ever
  • Unsubscribe anytime
  • 5-minute read

We never share your address. One click to leave.