Intuition What this page is for
The parent note gave you the five factory machines: (Hinglish version) — see 5.4.02 Array creation — np.zeros, np.ones, np.linspace, np.arange, np.random (Hinglish) and the English parent. Here we do the opposite of theory: we throw every awkward case at those machines and watch what comes out. By the end you will have seen , not just read, what happens at the edges — a zero-length array, a backwards range, a float step that misbehaves, a 2-D grid, a reproducible dice roll.
Every symbol used below is defined the moment it appears. If you have never seen NumPy, start at line one and you will still follow.
Before anything: three tiny words we will reuse constantly, each anchored to a picture.
Every row is a case class — a genuinely different situation these functions can face. Every worked example below is tagged with the cell(s) it covers, so you can check nothing is missing.
#
Case class
What is tricky
Covered by
C1
Blank grid, 2-D, custom dtype
shape is ONE tuple; dtype slot
Ex 1
C2
Preallocate-then-fill
the fast scientific pattern
Ex 2
C3
linspace, endpoint included
divide by num−1
Ex 3
C4
linspace, endpoint excluded
divide by num
Ex 3
C5
linspace degenerate: num=1 and num=0
limiting/empty behaviour
Ex 4
C6
arange normal, exclusive stop
fencepost count
Ex 5
C7
arange with float step drift
rounding surprise
Ex 6
C8
arange negative / backwards step
sign of step
Ex 7
C9
arange empty (start ≥ stop, positive step)
zero-length array
Ex 7
C10
random reproducible + statistics of large sample
mean → 0.5, mean → loc
Ex 8
C11
Real-world word problem (plotting x-axis)
choose the right tool
Ex 9
C12
Exam twist: linspace vs arange same-looking call
endpoint trap
Ex 10
Prerequisite links you may want open: NumPy arrays — shape, dtype, ndim , Floating-point representation and rounding errors , Vectorization vs Python loops .
2 × 3 array of integer ones.
Statement: Call np.ones to get a 2-rows-by-3-columns array where every element is the whole number 1, not the decimal 1.0.
Forecast (guess first): What shape argument do you pass, and where does the dtype go?
Steps:
Decide the shape. Two rows, three columns → the pair (2, 3).
Why this step? The parent warns: shape is one single argument , a tuple. Writing np.ones(2, 3) would read 3 as the dtype slot and crash.
Choose the dtype. We want whole numbers → dtype=int.
Why this step? The default is float64 (decimals). Scientific math usually wants floats, but here the problem explicitly asks for integers.
Write it:
np.ones(( 2 , 3 ), dtype = int )
# array([[1, 1, 1],
# [1, 1, 1]])
Verify: Number of elements = 2 × 3 = 6 . Every element equals 1. The printout shows 1 with no decimal point , confirming integer dtype (a float would print 1.). Total sum should be 6 .
Worked example Fill a length-5 buffer with the cubes
k 3 for k = 0 , 1 , 2 , 3 , 4 .
Statement: Produce the array [0, 1, 8, 27, 64] using the preallocate-then-fill pattern.
Forecast: Before running — what is 4 3 ? Will the buffer start as floats or ints?
Steps:
Make the empty buffer: out = np.zeros(5).
Why this step? A fixed-size buffer never has to grow. Growing a Python list and converting is slow — this is the pattern real scientific code uses (see Vectorization vs Python loops ).
Loop and assign each cube:
out = np.zeros( 5 )
for k in range ( 5 ):
out[k] = k ** 3
# out -> [ 0., 1., 8., 27., 64.]
Why this step? range(5) yields 0,1,2,3,4 — exactly the indices of a length-5 array. Index and value line up.
3. Read the result. Note the decimal points: out was created by np.zeros, whose default dtype is float , so the cubes are stored as 0., 1., 8., ….
Why this matters? If you needed integer output you would create np.zeros(5, dtype=int) instead.
Verify: The cubes are 0 , 1 , 8 , 27 , 64 . Their sum is 0 + 1 + 8 + 27 + 64 = 100 . Length stays 5 (preallocation never changes size).
num−1 and not num?
Picture a ruler from start to stop. If you paint num marks and keep both ends , the marks create num−1 gaps between them. Gaps, not marks, set the spacing Δ . Look at the figure: 5 marks, 4 gaps.
Worked example Same call, two endpoint settings.
Statement: Compare np.linspace(0, 10, 5) (endpoint kept) with np.linspace(0, 10, 5, endpoint=False).
Forecast: Which one includes the number 10? Which one has the bigger spacing?
Steps:
Endpoint=True (the default). Spacing Δ = 5 − 1 10 − 0 = 4 10 = 2.5 .
Why divide by 4? 5 points → 4 gaps.
Result: [0. , 2.5, 5. , 7.5, 10. ] — 10 is present .
Endpoint=False. Now the stop is thrown away , so the 5 points must fit into num gaps. Spacing Δ = 5 10 − 0 = 2.0 .
Why divide by 5? Dropping the endpoint means we behave like the left half of arange: num equal gaps.
Result: [0., 2., 4., 6., 8.] — 10 is absent .
Verify: True-case last element = 0 + 4 × 2.5 = 10 ✓ (equals stop). False-case last element = 0 + 4 × 2.0 = 8 ✓ (one gap short of 10). Both arrays have length 5.
Worked example What happens at the edge cases
num=1 and num=0?
Statement: Evaluate np.linspace(3, 9, 1) and np.linspace(3, 9, 0).
Forecast: If you ask for exactly ONE point between 3 and 9, which one do you get? And ZERO points?
Steps:
num=1: the formula would divide by num−1 = 0 — undefined! So NumPy sidesteps it: with one point there are no gaps , and it simply returns the start : [3.].
Why start and not stop? By definition the sequence begins at start; one sample means "just the beginning".
num=0: you asked for no points. NumPy returns an empty array [] of shape (0,).
Why is an empty array legal? Shape (0,) is a perfectly valid container holding zero elements — useful as a base case in loops. See NumPy arrays — shape, dtype, ndim .
Verify: np.linspace(3,9,1) has length 1 and its only value is 3.0. np.linspace(3,9,0) has length 0. Neither raises an error.
arange = Python range, but for decimals and NumPy arrays.
You give a start, a stop, and a step. It walks from start, adds step each time, and stops before reaching stop (stop is excluded — exactly like range).
np.arange(2, 11, 3)
Statement: List the elements and count them.
Forecast: Guess the last value. Is 11 included?
Steps:
Walk it: start at 2, add 3 each time → 2, 5, 8, then 11 would be next.
Why stop there? 11 equals stop, and stop is exclusive , so 11 is dropped.
Count with the formula: N = ⌈( 11 − 2 ) /3 ⌉ = ⌈ 9/3 ⌉ = ⌈ 3 ⌉ = 3 .
Why ceiling? If the division isn't a whole number, you still get the partial final step, so you round up. Here it's exactly 3.
Verify: Elements [2, 5, 8], count 3 ✓ matches formula. Last element 8 < 11 ✓ (stop excluded).
Common mistake "Ten steps of 0.1 must give exactly 10 clean values."
It feels airtight — 1/0.1 = 10 . But the computer cannot store 0.1 exactly (see Floating-point representation and rounding errors ). Tiny errors pile up, so the last value may sneak in or fall short, and the length becomes unpredictable .
np.arange(0, 1, 0.1) — how many elements really?
Statement: Does it stop at 0.9 (length 10) or does a stray 1.0-ish value appear (length 11)?
Forecast: Guess the length before reading on.
Steps:
Apply the count formula naively : N = ⌈( 1 − 0 ) /0.1 ⌉ = ⌈ 10 ⌉ = 10 .
Why "naively"? Because 0.1 stored in the machine is really 0.1000000000000000055…, slightly bigger than 0.1. So ( 1 − 0 ) /0.1 is slightly less than 10, and the last accumulated point 9 × 0.1 lands at 0.9000000000000001, still below 1, so it is included.
In practice NumPy gives 10 elements here: [0. , 0.1, …, 0.9], but with a different start/stop the accumulated error can flip it to 11.
Why is this dangerous? You cannot look at the call and be sure of the count. That unpredictability is the whole problem.
Fix: when you know how many points you want over an interval, use np.linspace(0, 1, 10, endpoint=False). It computes each point as start + k*Δ from scratch (no error accumulation), so the count is guaranteed.
Why linspace here? It answers "how MANY", which is exactly the guarantee we need.
Verify: np.arange(0,1,0.1) gives length 10 on standard NumPy. np.linspace(0,1,10,endpoint=False) gives length 10 with spacing exactly 0.1 and last element 0.9 .
sign of step decides direction.
A positive step walks upward and stops when it would pass stop from below. A negative step walks downward — so now start must be bigger than stop, and it stops when it would pass stop from above. Mismatch the signs and you get nothing .
Worked example Three sign scenarios.
Statement: Evaluate (a) np.arange(10, 0, -2), (b) np.arange(5, 5, 1), (c) np.arange(5, 2, 1).
Forecast: Which of these returns an empty array?
Steps:
(a) Backwards, matched signs: start 10, step −2, stop 0 (exclusive). Walk down: 10, 8, 6, 4, 2, then 0 would be next but 0 is the excluded stop.
Why does it work? start > stop AND step < 0 agree on the direction.
Result: [10, 8, 6, 4, 2], length 5.
(b) start equals stop: np.arange(5, 5, 1). There is no room — you start at 5 but must stop before 5. Zero elements.
Why? The interval [ 5 , 5 ) is empty.
Result: [], shape (0,).
(c) Positive step but start > stop: np.arange(5, 2, 1). You want to move up from 5, but must stop before reaching 2 — you are already past it. Zero elements.
Why? Direction of step (up) contradicts start>stop; nothing qualifies.
Result: [], shape (0,).
Verify: (a) length 5, elements [10,8,6,4,2], sum 30 . (b) length 0. (c) length 0. No errors — empty arrays are valid.
Definition The modern generator
rng = np.random.default_rng(seed) builds a Generator — an object that produces random numbers. Feeding it a seed (any fixed integer) means the same seed always yields the same sequence, so results replay exactly. Then rng.random(n) gives n uniform floats in [ 0 , 1 ) ; rng.normal(loc, scale, n) gives n Gaussian numbers with mean loc, standard deviation scale. More in Random sampling and distributions .
Worked example Predict the mean of a large uniform sample.
Statement: With rng = np.random.default_rng(0), take x = rng.random(100000). What is x.mean() approximately, and what exact 3 numbers does rng.random(3) give first?
Forecast: A uniform number lands anywhere in [ 0 , 1 ) with equal chance. Where is its "balance point"?
Steps:
Reproducibility: because the seed is 0, the first three draws are a fixed triple. On this NumPy generator they are approximately [0.63696169, 0.26978671, 0.04097352].
Why check exact numbers? To prove "same seed → same output" — anyone running this gets these digits.
Mean of many uniforms: uniform on [ 0 , 1 ) has its balance point in the middle → 0.5 . With 100 000 samples the average should sit very close to 0.5.
Why so close? The Law of Large Numbers: more samples → sample mean hugs the true mean.
Normal check: rng.normal(0, 1, 100000).mean() should be near loc = 0, and its standard deviation near scale = 1.
Why? loc and scale literally are the mean and spread of the Gaussian.
Verify: np.random.default_rng(0).random(3) matches the triple above to 8 decimals. The mean of 100 000 uniform draws is within 0.01 of 0.5 . The mean of 100 000 normal(0,1) draws is within 0.01 of 0 .
sin ( x ) smoothly on [ 0 , 2 π ] .
Statement: You want a smooth curve of sin ( x ) from 0 to 2 π using exactly 100 sample points, both ends included. Which creator, and what call?
Forecast: Do you care about the step size , or about getting a fixed number of neat points including both ends?
Steps:
Identify the need: "exactly 100 points, endpoints included, over an interval." That is the definition of linspace, not arange.
Why not arange? You'd have to compute a step 2 π /99 and risk float drift — the wrong tool for "how many". See Plotting functions with Matplotlib .
Write:
x = np.linspace( 0 , 2 * np.pi, 100 ) # 100 points, includes 0 and 2π
y = np.sin(x)
Spacing is Δ = 100 − 1 2 π − 0 = 99 2 π .
Why 99? 100 points → 99 gaps.
Verify: x has length 100; first element 0 ; last element 2 π ≈ 6.2832 (endpoint kept). Spacing 2 π /99 ≈ 0.06347 . And sin ( x [ 0 ]) = 0 , sin ( x [ − 1 ]) = sin ( 2 π ) ≈ 0 .
np.linspace(0, 10, 10) vs np.arange(0, 10) — same-looking, wildly different.
Statement: For each, give the length, the step between consecutive elements, and whether 10 appears.
Forecast: They both mention 0 and 10. Do they have the same step?
Steps:
np.linspace(0, 10, 10): 10 points, endpoint included . Spacing Δ = 10 − 1 10 − 0 = 9 10 ≈ 1.111 .
Why not step 1? The classic trap: "0 to 10 in 10" looks like step 1, but 10 points make 9 gaps, so the step is 10/9 . The value 10. is the last element.
np.arange(0, 10): with a single-missing step it defaults to step 1, giving 0,1,…,9. Length 10, step exactly 1, and 10 is absent (stop excluded).
Why absent? arange stop is exclusive like Python range.
To actually get step-1 with the endpoint 10 present, use np.linspace(0, 10, 11) or np.arange(0, 11).
Verify: linspace(0,10,10): length 10, last element 10.0, step 10/9 ≈ 1.1111 . arange(0,10): length 10, last element 9, step 1. arange(0,11): last element 10.
Recall Rapid self-test
Length of np.arange(2,11,3)? ::: 3 → [2,5,8] (stop 11 excluded).
np.linspace(0,10,5) step? ::: 10/4 = 2.5 , includes 10.
np.linspace(0,10,5,endpoint=False) step? ::: 10/5 = 2.0 , excludes 10.
np.linspace(3,9,1) returns? ::: [3.] — just the start (no gaps).
np.linspace(3,9,0) returns? ::: [], shape (0,).
np.arange(5,5,1) returns? ::: empty array [].
np.arange(5,2,1) returns? ::: empty (positive step but start>stop).
np.arange(10,0,-2) returns? ::: [10,8,6,4,2].
Mean of many rng.random() draws? ::: 0.5.
Right tool for "100 points on [ 0 , 2 π ] , ends included"? ::: np.linspace.
Parent: 5.4.02 Array creation — np.zeros, np.ones, np.linspace, np.arange, np.random (Hinglish)
NumPy arrays — shape, dtype, ndim
Vectorization vs Python loops
Array indexing and slicing
Reshaping — reshape, ravel, newaxis
Plotting functions with Matplotlib
Random sampling and distributions
Floating-point representation and rounding errors