PYTHON / NUMPY
Creating arrays and choosing dtypes
Create NumPy arrays with array, zeros, arange and linspace, and choose a dtype deliberately so range, precision and memory fit your data.
What you will learn
- Pass dtype= at creation instead of trusting inference from Python literals
- Read .dtype, .itemsize and .nbytes to know exactly how an array is stored
- Remember astype returns a copy and float-to-int casts truncate toward zero
- Use linspace for float ranges; arange with a float step can overshoot stop
Understanding Creating arrays and choosing dtypes
A NumPy array stores one machine type for every element, decided once when the array is built and never changed afterwards. When you call np.array on a Python list, NumPy scans the whole list and picks the narrowest type that can hold everything, promoting along the chain bool to int to float to complex. That is why np.array([1, 2, 3.5]) comes back as float64 rather than a mix: a single block of memory cannot hold two different element types.
The dtype is a layout contract, not a label. int16 means 'two bytes per element, two's complement', so an int16 array of six values occupies exactly 12 bytes and can only represent -32768 through 32767. float32 means four bytes with about seven decimal digits of precision. Choosing a dtype is therefore choosing a range, a precision and a memory footprint at the same time, and NumPy will not rescue you at runtime if a value does not fit.
The creation helpers have their own defaults worth memorising: np.zeros, np.ones, np.empty and np.linspace give float64; np.full copies the type of the fill value; np.arange infers from its arguments, so np.arange(5) is integer but np.arange(5.0) is float. Any of them accepts dtype= to override. To change an existing array you use astype, which always allocates a new array, so its return value must be assigned; and np.zeros_like or np.empty_like reuse the dtype and shape of an array you already have.
import numpy as np
ints = np.array([1, 2, 3])
mixed = np.array([1, 2, 3.5])
forced = np.array([1, 2, 3], dtype=np.float32)
print(ints.dtype, ints.itemsize)
print(mixed.dtype, mixed)
print(forced.dtype, forced.nbytes)
grid = np.zeros((2, 3), dtype=np.int16)
print(grid)
print(grid.dtype, grid.nbytes)
steps = np.linspace(0, 1, 5)
print(steps, steps.dtype)An array's dtype is a fixed per-element memory layout chosen at creation, and range, precision and size all follow from it.
Worked examples
Casting is lossy in specific, silent ways
Shows how astype truncates floats, wraps integers that do not fit, and collapses values to bool.
import numpy as np
prices = np.array([1.9, -1.9, 2.5])
print(prices.astype(np.int32))
counts = np.array([1_000_000, 2_000_000])
print(counts.astype(np.int16))
flags = np.array([0, 3, -1]).astype(bool)
print(flags)Example explained
Line 1astype(np.int32) drops the fraction toward zero, so -1.9 becomes -1 and 2.5 becomes 2; this is truncation, not rounding.
Line 21000000 does not fit in two bytes, so only the low 16 bits survive and 2000000 comes back negative, with no error or warning.
Line 3Casting to bool maps every nonzero value to True, which is why -1 becomes True rather than False.
Fixed-width string dtypes
Demonstrates that a text array reserves a fixed number of characters per element, sized from the input.
import numpy as np
names = np.array(["ada", "grace", "hopper"])
print(names.dtype)
names[0] = "alexandra"
print(names)
wide = names.astype("<U12")
wide[0] = "alexandra"
print(wide.dtype, wide)Example explained
Line 1The inferred dtype <U6 means six Unicode characters per slot, taken from the longest input string 'hopper'.
Line 2Assigning a nine-character name cannot enlarge the slot, so it is cut down to 'alexan' silently.
Line 3astype('<U12') allocates wider slots, and the same assignment now stores the full name.
float32 loses digits permanently
Shows the memory saving of float32 and that casting back to float64 does not recover precision.
import numpy as np
x = np.array([1 / 3, 2 / 3], dtype=np.float64)
y = x.astype(np.float32)
print(x.nbytes, y.nbytes)
print(x[0], y[0])
print(y.astype(np.float64)[0])Example explained
Line 1x holds two 8-byte values (16 bytes); y holds two 4-byte values, halving memory.
Line 2float32 keeps roughly seven decimal digits, so y[0] prints as 0.33333334.
Line 3Widening back to float64 only reveals the exact float32 bit pattern; the discarded digits are gone for good.
arange versus linspace for float ranges
Compares how the two functions decide element count and dtype for a fractional range.
import numpy as np
a = np.arange(1, 1.3, 0.1)
print(a.size, a)
b = np.linspace(1, 1.3, 3)
print(b.size, b)
print(np.arange(4).dtype, np.arange(4.0).dtype, np.linspace(0, 1, 3).dtype)Example explained
Line 1arange computes the count as ceil((stop - start) / step) in floating point, and rounding error makes it 4 here, so the excluded stop value appears anyway.
Line 2linspace takes the count directly and includes both endpoints, so the result length is exactly what you asked for.
Line 3arange's dtype follows its arguments (integer in, integer out), while linspace always produces floats.
Important notes
The default integer dtype is int64 on Linux and macOS, and on Windows with NumPy 2.0+, but int32 on Windows with NumPy 1.x. Write dtype=np.int64 explicitly when you actually need values past two billion.
np.empty does not zero the memory it hands you, so its contents are whatever was in that block before; only use it when you overwrite every element.
Common mistakes
Storing a float into an integer array, as in a = np.zeros(3, dtype=np.int32); a[0] = 21.5. The dtype is fixed, so 21 is stored and the .5 disappears with no error.
Calling arr.astype(np.float64) without assigning the result and then wondering why arr is still integer; astype builds a new array and leaves the original untouched.
Passing a ragged nested list such as [[1, 2], [3]] to np.array expecting a 2D array. NumPy cannot pick one rectangular layout, so modern versions raise ValueError instead of quietly making an object array.
Try it yourself
Change, predict, then run
Create a 3x4 array of zeros with dtype np.int16, print its nbytes, assign 3.9 to element [0, 0] and print the array to see what was stored. Then make a float32 copy with astype and print the copy's dtype and nbytes.
Open the Python workspaceCheck your understanding
You build temps = np.zeros(4) and counts = np.zeros(4, dtype=np.int32), then run temps[0] = 21.5 and counts[0] = 21.5. What do the two arrays hold at index 0?
- temps[0] is 21.5 and counts[0] is 21, because the value is cast to int32 on assignment
- Both hold 21.5, because NumPy promotes counts to float when a float is stored
- temps[0] is 21.5 and the assignment to counts raises a TypeError
- Both hold 21, because np.zeros arrays only accept whole numbers until you call astype
Show answer
An array's dtype is fixed at creation, so writing into counts casts 21.5 to int32 by truncating toward zero and stores 21 without complaint. The promotion option is tempting because arithmetic between arrays does promote types, but promotion always produces a new array; it never rewrites the dtype of an existing one, and np.zeros(4) is float64 so temps keeps the full 21.5.