datarekha

Creating arrays

zeros, ones, arange, linspace, random — the constructors you'll reach for every day, and when to use which.

6 min read Beginner NumPy Lesson 3 of 14

What you'll learn

  • The standard "shape-first" constructors — zeros, ones, full, empty
  • The difference between arange (step) and linspace (count)
  • Why `np.empty` is faster but dangerous
  • Random arrays for weights, simulations, and test data

Before you start

The last lesson left you typing arrays out by hand — np.array([4.5, 1.2, ...]) — and then admitted you will almost never do that in real code. You don’t have the numbers; you have a shape you want filled. A placeholder for 128 embeddings. A bias vector of ones. A range of x-values for a plot. Random weights for an untrained model. This lesson is the toolbox of constructors that conjure an array from a shape alone — six of them cover about 95% of what you will ever reach for.

Filled arrays — zeros, ones, full

The simplest pattern: ask for a shape, and tell NumPy what to fill it with.

import numpy as np

# Placeholder for 128 embedding vectors of dim 768 (e.g., BERT outputs).
embeddings = np.zeros((128, 768), dtype=np.float32)
print("embeddings:", embeddings.shape, embeddings.dtype)

# Bias terms initialized to 1.0
bias = np.ones(768, dtype=np.float32)
print("bias      :", bias.shape, "sum =", bias.sum())

# A "mask" filled with a sentinel value (e.g., -1 for missing label).
labels = np.full((128,), fill_value=-1, dtype=np.int32)
print("labels    :", labels[:5], "...")
embeddings: (128, 768) float32
bias      : (768,) sum = 768.0
labels    : [-1 -1 -1 -1 -1] ...

The pattern is always the same: pass the shape first, then an optional dtype. The default dtype is float64, which is almost never what you want for big data — so be explicit.

import numpy as np

# DON'T do this:
buf = np.empty((3, 4))
print("uninitialized 'empty':")
print(buf)   # could be anything — old memory contents.

# DO this when you'll fill it immediately:
result = np.empty((1000,), dtype=np.float64)
for i in range(1000):
    result[i] = i ** 0.5   # we overwrite every slot before reading
print("first 5 filled:", result[:5])
uninitialized 'empty':
[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]
first 5 filled: [0.         1.         1.41421356 1.73205081 2.        ]

On this particular run the uninitialized buffer happened to come back all zeros — freshly-allocated memory often does, which is exactly what makes empty so treacherous. It is luck, not a guarantee: reuse a buffer later in a program and np.empty will hand you stale leftover values instead. The DO half is the safe pattern — every slot is written (i ** 0.5) before anything reads it, so the result is the clean [0, 1, √2, √3, 2, …].

Ranges — arange vs linspace

These two cover almost every “give me a sequence” need.

  • np.arange(start, stop, step) — like Python’s range, exclusive upper bound, you choose the step.
  • np.linspace(start, stop, num) — inclusive on both ends, you choose the count.
import numpy as np

# arange: step = 0.5, stop is exclusive.
print("arange  :", np.arange(0, 3, 0.5))      # [0., 0.5, 1., 1.5, 2., 2.5]

# linspace: 7 points from 0 to 2π — exactly what you want for plotting sin(x).
x = np.linspace(0, 2 * np.pi, 7)
print("linspace:", x.round(3))

# Why linspace beats arange for floats: arange can miss the endpoint
# due to floating-point rounding.
print("arange to 1.0:", np.arange(0, 1.0, 0.1))       # 10 points, no 1.0
print("linspace 11  :", np.linspace(0, 1.0, 11))       # 11 points, includes 1.0
arange  : [0.  0.5 1.  1.5 2.  2.5]
linspace: [0.    1.047 2.094 3.142 4.189 5.236 6.283]
arange to 1.0: [0.  0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9]
linspace 11  : [0.  0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]

Rule of thumb: integer step → arange. Float-spaced grid for plotting or numerical integration → linspace. The reason arange drops the endpoint is the floating-point trap from the numerical-stability lesson: it accumulates start + k·step, and rounding can nudge the final value just past stop, so it is dropped. linspace sidesteps this by computing each point independently as start + k·(stop − start)/(num − 1) — which is why its endpoint is exact.

Identity and diagonals

import numpy as np

# Identity matrix — used in linear algebra and as a starting point for
# many transformations.
I = np.eye(4, dtype=np.float32)
print("eye(4):")
print(I)

# np.identity is the square-only variant — same result here.
print("identity(3):")
print(np.identity(3))
eye(4):
[[1. 0. 0. 0.]
 [0. 1. 0. 0.]
 [0. 0. 1. 0.]
 [0. 0. 0. 1.]]
identity(3):
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]

Random arrays — the modern API

The old np.random.rand(...) calls still work, but the modern way is to create a Generator from np.random.default_rng(seed). It is faster, has more distributions, and — given a seed — is perfectly reproducible.

import numpy as np

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

# Uniform [0, 1) — good for stochastic masks, dropout.
u = rng.random((3, 4))
print("uniform:")
print(u.round(3))

# Gaussian (standard normal) — most common weight init.
w = rng.standard_normal((4, 4))
print("\nstandard normal:")
print(w.round(2))

# Integer labels — useful for fake datasets, batch sampling.
y = rng.integers(low=0, high=10, size=20)
print("\nintegers 0..9:", y)
uniform:
[[0.179 0.64  0.467 0.371]
 [0.355 0.791 0.905 0.177]
 [0.653 0.298 0.967 0.92 ]]

standard normal:
[[-0.06 -0.09  0.16 -0.61]
 [-0.4   0.55 -0.13 -1.37]
 [-0.48  0.66 -0.23 -0.15]
 [ 0.64  1.82 -0.71  1.35]]

integers 0..9: [3 4 6 2 9 2 9 8 9 7 5 6 2 3 4 9 7 5 5 4]

Because the seed is fixed at 2026, those exact numbers reproduce on any machine — and note rng.integers(low=0, high=10, ...) returns values 0…9: the high bound is exclusive, so 10 never appears.

Decision tree

Quick reference for “which constructor do I want?”:

Need filler value 0?              → np.zeros(shape, dtype=...)
Need filler value 1?              → np.ones(shape, dtype=...)
Need a different filler?          → np.full(shape, value, dtype=...)
Need uninitialized (fast)?        → np.empty(shape)  — but fill it!
Need a stepped range?             → np.arange(start, stop, step)
Need N equally-spaced floats?     → np.linspace(start, stop, num)
Need an identity matrix?          → np.eye(n)
Need random values?               → rng = default_rng(); rng.random(shape)

In one breath

The shape-first constructors fill an array from a shape alone: zeros / ones / full (value-filled), empty (uninitialized — fast but dangerous, only safe if you overwrite every slot before reading), arange (you pick the step, upper bound exclusive, can miss a float endpoint to rounding) vs linspace (you pick the count, both ends inclusive, endpoint exact), eye/identity (identity matrix), and random via the modern default_rng(seed) generator (.random, .standard_normal, .integers with an exclusive high). The default dtype is float64 — almost never what you want at scale, so pass dtype= explicitly.

Practice

Quick check

0/3
Q1You need a 1D array of exactly 100 points from 0 to 1, including both endpoints. Which is correct?
Q2Why is `np.empty` faster than `np.zeros`?
Q3What's the difference between `np.random.rand(3,3)` and `np.random.default_rng().random((3,3))`?

A question to carry forward

Did you catch the refrain running through every constructor in this lesson? Pass the dtype. The default float64 is almost never what you want. Be explicit. We kept issuing that warning and never once justified it — what does the dtype actually buy you, what does it cost, and what breaks if you ignore it?

The next lesson finally opens that box. What are the NumPy dtypes — how much memory and how much numeric range each one gives you — and what are the silent, no-exception-raised overflow and precision bugs they invite? The int8 that wraps around from 127 to −128 without a peep, the float32 that quietly rounds your sum into noise: these are among the most baffling bugs in all of numeric Python, precisely because nothing errors out.

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Practice this in an interview

All questions
When would you use a Python list versus a NumPy array, and what are the performance trade-offs?

Python lists are heterogeneous, pointer-based, and general-purpose. NumPy arrays are homogeneous, stored as contiguous typed memory, and support vectorised operations that run at C speed. For numerical work on more than a few hundred elements, NumPy is almost always faster and more memory-efficient.

Why is NumPy significantly faster than Python for-loops for numerical computation, and what is vectorization?

NumPy operations execute compiled C code over contiguous memory blocks in a single call, while a Python loop incurs interpreter overhead and dynamic type checks on every element. Vectorization means expressing an operation over an entire array at once so the hot path never re-enters the Python interpreter.

When should you use apply, map, or applymap versus vectorized pandas operations, and what are the performance implications?

Vectorized pandas and NumPy operations operate on entire arrays in compiled C/Fortran code and should always be your first choice. apply runs a Python function row- or column-wise in a Python loop, map transforms a single Series element-by-element, and applymap (DataFrame.map in pandas 2.1+) applies a function to every scalar — all three are orders of magnitude slower than vectorized equivalents.

Why does weight initialization matter and how do Xavier and He initialization work?

Poor initialization causes the variance of activations to either explode or collapse across layers, triggering vanishing or exploding gradients before training even begins. Xavier initialization targets variance preservation for saturating activations; He initialization corrects for the halved variance caused by ReLU zeroing negative inputs.

Related lessons

Explore further

Skip to content