Skip to main content
NumPy beginner Lesson 3 of 12

Creating NumPy Arrays

Master every array creation method — from Python sequences to ranges, zeros, ones, random values, and structured data.

Real-World Scenario

A data engineer building a time-series analysis pipeline needs to initialize weight matrices for a neural network, generate evenly spaced frequency bins for an FFT, and pre-allocate an output buffer before streaming data into it. Each task calls for a different array creation method — knowing all of them prevents the common trap of converting Python lists through slow intermediate steps.

Creating Arrays from Python Data

The most direct route: pass a Python list or nested list to np.array(). NumPy infers the dtype from the contents.

import numpy as np

# 1-D from list
prices = np.array([10.5, 20.0, 15.75, 8.25])
print(prices.dtype)  # float64

# 2-D from nested list — shape is (rows, cols)
matrix = np.array([
    [1, 2, 3],
    [4, 5, 6]
])
print(matrix.shape)  # (2, 3)

# Force a specific dtype — important when memory is constrained
prices_f32 = np.array([10.5, 20.0, 15.75], dtype=np.float32)
print(prices_f32.nbytes)  # 12 bytes vs 24 for float64

# Boolean array — useful for masking operations
flags = np.array([True, False, True, True])
print(flags.dtype)  # bool

Range-Based Arrays

import numpy as np

# np.arange(start, stop, step) — like Python's range() but returns an ndarray
# stop is exclusive
indices = np.arange(0, 10, 2)
print(indices)  # [0 2 4 6 8]

# Float step is valid (unlike range())
time_steps = np.arange(0.0, 1.0, 0.1)
print(time_steps)
# [0.  0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9]

# np.linspace(start, stop, num) — num evenly spaced points, endpoint included
# Use this for plotting axes and signal sampling
freq_bins = np.linspace(0, 1000, 11)  # 11 points from 0 to 1000 Hz
print(freq_bins)
# [   0.  100.  200.  300.  400.  500.  600.  700.  800.  900. 1000.]

# np.logspace — logarithmically spaced (useful for learning rate searches)
lr_grid = np.logspace(-5, -1, 5)  # 10^-5 to 10^-1
print(lr_grid)
# [1.e-05 1.e-04 1.e-03 1.e-02 1.e-01]

Pre-filled Arrays

These are the fastest way to allocate and initialize large arrays because NumPy calls a single C memory function.

import numpy as np

# All zeros — safe default for output buffers
weights = np.zeros((3, 4))       # 3×4 matrix of float64 zeros
print(weights.shape)  # (3, 4)

# All ones — useful for multiplicative identity or mask initialization
ones = np.ones((2, 3), dtype=np.int32)

# Fill with a specific value
bias = np.full((5,), fill_value=0.01)
print(bias)  # [0.01 0.01 0.01 0.01 0.01]

# Identity matrix — essential for linear algebra
eye = np.eye(4)          # 4×4 identity matrix
print(eye)
# [[1. 0. 0. 0.]
#  [0. 1. 0. 0.]
#  [0. 0. 1. 0.]
#  [0. 0. 0. 1.]]

# np.empty — allocate without initializing (must overwrite before reading)
output_buffer = np.empty((1000, 1000))  # faster than zeros for output-only arrays

Arrays from Shape of Existing Arrays

When writing functions that produce output the same size as an input, _like variants keep the code readable and dtype-consistent.

import numpy as np

data = np.array([[1.0, 2.0], [3.0, 4.0]])

zeros_like  = np.zeros_like(data)   # same shape and dtype as data, filled with 0
ones_like   = np.ones_like(data)    # same shape and dtype, filled with 1
empty_like  = np.empty_like(data)   # same shape and dtype, uninitialized

print(zeros_like)
# [[0. 0.]
#  [0. 0.]]

Random Arrays

import numpy as np

rng = np.random.default_rng(seed=42)  # reproducible random number generator

# Uniform floats in [0, 1)
uniform = rng.random((3, 3))

# Standard normal (mean=0, std=1) — default weight initialization in ML
normal = rng.standard_normal((100, 50))

# Integers in [low, high)
labels = rng.integers(0, 10, size=(20,))

# Normal with custom mean and std
signal = rng.normal(loc=5.0, scale=2.0, size=(1000,))
print(f"Mean: {signal.mean():.2f}, Std: {signal.std():.2f}")
# Mean: 5.02, Std: 1.99

# Random choice — sampling from an existing array
population = np.arange(100)
sample = rng.choice(population, size=10, replace=False)
print(sample)  # 10 unique values from 0–99

Structured and Diagonal Arrays

import numpy as np

# Diagonal matrix from a 1-D array
values = np.array([1, 2, 3, 4])
diag_matrix = np.diag(values)
print(diag_matrix)
# [[1 0 0 0]
#  [0 2 0 0]
#  [0 0 3 0]
#  [0 0 0 4]]

# Extract the diagonal from a 2-D array
matrix = np.array([[10, 2, 3], [4, 50, 6], [7, 8, 90]])
print(np.diag(matrix))  # [10 50 90]

Array Creation Cheatsheet

FunctionUse case
np.array(data)From Python list/tuple
np.arange(start, stop, step)Integer/float range
np.linspace(start, stop, n)N evenly spaced points
np.zeros(shape)Zero-filled
np.ones(shape)One-filled
np.full(shape, val)Constant value
np.eye(n)Identity matrix
np.empty(shape)Uninitialized (fast buffer)
rng.random(shape)Uniform floats [0, 1)
rng.standard_normal(shape)Gaussian samples

Frequently Asked Questions

What is the difference between np.zeros and np.empty?
np.zeros fills the allocated memory with zeros. np.empty allocates memory without initializing it — the values are whatever bytes were already at that address. np.empty is slightly faster, but you must overwrite every element before reading from it or you'll get garbage values.
When should I use np.arange vs np.linspace?
Use np.arange when you know the step size (like range()). Use np.linspace when you know how many points you want between two endpoints — it guarantees the endpoint is included and distributes values evenly, which is important for plotting and signal processing.