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

- **Author:** Vanshika Nigam — ETL & Data Engineer, Accenture (https://www.1stepgrow.com/authors/vanshika-nigam/)
- **Published:** Aug 7, 2026 · **Updated:** Sep 17, 2026
- **Topic:** Python & Programming · **Format:** Guide · **Read time:** 9 min
- **Canonical URL:** https://www.1stepgrow.com/articles/numpy-tutorial/

## Key takeaways

- A NumPy array is a fixed-type, contiguous block of memory, and that layout is the main reason it is fast.
- Every array has one dtype. Mixing types silently upcasts, so a single stray string turns a numeric array into text.
- Vectorised operations replace loops; if you are writing a for loop over an array, there is usually a shorter and faster way.
- Shape is the concept everything else in NumPy builds on, so check it first when an error makes no sense.

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](https://www.1stepgrow.com/articles/numpy-indexing-slicing) moves on to indexing and slicing. If you have not set up Python yet, follow the [Anaconda on Windows guide](https://www.1stepgrow.com/articles/install-anaconda-windows) or the [macOS version](https://www.1stepgrow.com/articles/install-anaconda-mac) 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](https://numpy.org/doc/stable/).

## 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](https://numpy.org/doc/stable/release/2.0.0-notes.html). 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](https://numpy.org/doc/stable/reference/generated/numpy.arange.html) 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](https://numpy.org/doc/stable/reference/random/generator.html) 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](https://www.1stepgrow.com/articles/numpy-aggregation-functions) 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](https://www.1stepgrow.com/articles/numpy-indexing-slicing) 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.



## Related reading

[NumPy Part 2: array indexing and slicing](https://www.1stepgrow.com/articles/numpy-indexing-slicing) continues the series, then [Part 3 on broadcasting](https://www.1stepgrow.com/articles/numpy-broadcasting). For how Python's own number types behave, see [Python numeric data types](https://www.1stepgrow.com/articles/python-numeric-data-types). For where NumPy sits in the wider stack, see [tools and techniques for data science](https://www.1stepgrow.com/tools-and-techniques-for-data-science), and for a one-page summary the [NumPy cheat sheet](https://www.1stepgrow.com/articles/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.

---
_Source: 1stepGrow (https://www.1stepgrow.com/articles/numpy-tutorial/). Cite with the title, "1stepGrow" and a link._
