Skip to main content
NumPy · lesson 3 of 13
NumPy beginner Lesson 3 of 13

Creating NumPy Arrays

Create NumPy arrays from lists, ranges, linspace, zeros, ones, full, eye and random generators, and see the float-step arange trap with real output.

By SkillByExample · Updated

From Python Lists

np.array() turns a list, or a nested list, into an array and infers one dtype for all of it.

import numpy as np

prices = np.array([10.5, 20.0, 15.75, 8.25])
matrix = np.array([[1, 2, 3], [4, 5, 6]])
mixed = np.array([1, 2.5, 3])
small = np.array([10.5, 20.0, 15.75], dtype=np.float32)

print(prices.dtype, matrix.shape, matrix.dtype)
print(mixed, mixed.dtype)
print(small.nbytes, "bytes as float32")
$ python from_lists.py
float64 (2, 3) int64
[1.  2.5 3. ] float64
12 bytes as float32

mixed contained one float, so NumPy upcast the whole array to float64 — an array has a single dtype, and it picks one wide enough for every element. Passing dtype=np.float32 overrides inference: three 4-byte floats take 12 bytes instead of 24.

Ranges: arange and linspace

import numpy as np

print(np.arange(0, 10, 2))
print(np.linspace(0, 1000, 5))
print(np.logspace(-4, -1, 4))
$ python ranges.py
[0 2 4 6 8]
[   0.  250.  500.  750. 1000.]
[0.0001 0.001  0.01   0.1   ]

arange works like range (stop excluded). linspace asks for a count of points and includes both ends, which is what you want for plot axes and sample grids. logspace spaces points evenly in powers of ten — a common grid for learning-rate searches.

The Float-Step arange Trap

import numpy as np

a = np.arange(1, 1.3, 0.1)
print(a, len(a))
print(np.linspace(1, 1.2, 3))
print((1.3 - 1) / 0.1)
$ python arange_trap.py
[1.  1.1 1.2 1.3] 4
[1.  1.1 1.2]
3.0000000000000004

The stop value 1.3 was supposed to be excluded, yet it appears. arange computes its length as ceil((stop - start) / step), and the last line shows that division comes out a hair above 3 in floating point, so the ceiling is 4. linspace sidesteps the problem by taking the count directly.

Pre-filled Arrays

import numpy as np

print(np.zeros((2, 3)))
print(np.ones((2, 3), dtype=np.int32))
print(np.full(4, 0.01))
print(np.eye(3))
$ python prefilled.py
[[0. 0. 0.]
 [0. 0. 0.]]
[[1 1 1]
 [1 1 1]]
[0.01 0.01 0.01 0.01]
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]

zeros and eye default to float64 (note the trailing dots), while dtype=np.int32 made the ones array integer. Shapes are passed as tuples — (2, 3) — except for 1-D arrays, where a bare integer works.

Uninitialized Memory with np.empty

import numpy as np

buf = np.empty(5)
print("before:", buf)
buf[:] = np.arange(5) * 1.5
print("after: ", buf)
$ python empty.py
before: [5.82554639e-313 2.00806763e+289 1.45694829e-308 9.21286186e-318
 6.49172988e-309]
after:  [0.  1.5 3.  4.5 6. ]

The “before” line is not guaranteed to be zeros or anything else — np.empty hands you memory without clearing it, so its contents vary between runs and machines. It is only safe when every element is overwritten before being read, as the slice assignment does here.

Arrays Shaped Like Another Array

import numpy as np

data = np.array([[1.5, 2.0], [3.0, 4.5]])
print(np.zeros_like(data))
print(np.full_like(data, 7))
print(np.ones_like(data, dtype=int))
$ python like.py
[[0. 0.]
 [0. 0.]]
[[7. 7.]
 [7. 7.]]
[[1 1]
 [1 1]]

The _like functions copy the shape and dtype of an existing array, so full_like(data, 7) produced floats (7.) because data is float. Pass dtype= to override, as the last line does.

Random Arrays

import numpy as np

rng = np.random.default_rng(seed=42)

print(rng.random(3))
print(rng.integers(0, 10, size=5))
signal = rng.normal(loc=5.0, scale=2.0, size=1000)
print(f"mean {signal.mean():.2f}, std {signal.std():.2f}")
print(rng.choice(np.arange(100), size=5, replace=False))
$ python random_arrays.py
[0.77395605 0.43887844 0.85859792]
[0 6 2 0 5]
mean 4.95, std 1.97
[31 99 93 65 12]

With a fixed seed these exact numbers come back on every run. The sample mean and standard deviation land near, not exactly on, the requested 5.0 and 2.0 — a thousand draws is a sample, not the distribution. The NumPy Random lesson covers generators in depth.

Diagonals

import numpy as np

print(np.diag([1, 2, 3]))
m = np.array([[10, 2, 3], [4, 50, 6], [7, 8, 90]])
print(np.diag(m))
$ python diag.py
[[1 0 0]
 [0 2 0]
 [0 0 3]]
[10 50 90]

np.diag works in both directions: given a 1-D input it builds a square matrix with those values on the diagonal, and given a 2-D input it extracts the diagonal.

Practice

  1. Create 7 evenly spaced values from 0 to 3 inclusive, once with linspace and once with arange. Which call is easier to get right?
  2. Build a 3 × 3 integer array filled with -1.
  3. Show that np.arange(0, 0.3, 0.1) does not hit the trap. Print its length and explain.
Solution 1
import numpy as np

print(np.linspace(0, 3, 7))
print(np.arange(0, 3.5, 0.5))
$ python practice_seven.py
[0.  0.5 1.  1.5 2.  2.5 3. ]
[0.  0.5 1.  1.5 2.  2.5 3. ]

linspace takes the count directly. With arange you have to work out the step (0.5) and pick a stop beyond 3 so that 3 is included.

Solution 2
import numpy as np

print(np.full((3, 3), -1))
$ python practice_full.py
[[-1 -1 -1]
 [-1 -1 -1]
 [-1 -1 -1]]

np.full infers an integer dtype from the fill value -1.

Solution 3
import numpy as np

a = np.arange(0, 0.3, 0.1)
print(a, len(a))
print((0.3 - 0) / 0.1)
$ python practice_notrap.py
[0.  0.1 0.2] 3
2.9999999999999996

Here the division rounds down to just under 3, so the ceiling is 3 and the stop is excluded as intended. Whether the trap fires depends on which way the float division happens to round, which is exactly why a float-step arange is unreliable.

Frequently Asked Questions

What is the difference between np.zeros and np.empty?
np.zeros allocates memory and fills it with zeros. np.empty only allocates, so the array holds whatever bytes were already in that memory. np.empty saves the fill step, but every element must be written before it is read, otherwise the program reads garbage values.
When should I use np.arange vs np.linspace?
Use np.arange for integer ranges where you know the step, just like Python's range. Use np.linspace when you know how many points you want between two endpoints; it always includes the endpoint and never suffers from floating-point step rounding, which can make np.arange with a float step return one element more than expected.
Why does np.arange(1, 1.3, 0.1) include 1.3?
The number of elements np.arange returns is computed as ceil((stop - start) / step). With floats, (1.3 - 1) / 0.1 evaluates to slightly more than 3, so the ceiling is 4 and the last element lands on roughly 1.3 even though stop is meant to be exclusive. Use np.linspace(1, 1.2, 3) or an integer range scaled afterwards to avoid it.
How do I choose the dtype of a new NumPy array?
Every creation function accepts a dtype argument, for example np.zeros(5, dtype=np.int32) or np.array(values, dtype=np.float32). Without it NumPy infers float64 for decimals and int64 for integers. Choosing a smaller dtype such as float32 halves memory use, at the cost of precision.