The half-dozen constructors that cover almost everything, and which one to reach for when.
Overview
From data you already have
Every NumPy program starts by creating an array. The constructor you choose depends on where the data comes from and what shape you need. Getting the right constructor is a small decision, but the wrong one can give you an array with the wrong dtype, the wrong shape, or the wrong number of dimensions.
The most common path is np.array, which takes a list and infers the shape and dtype from its contents. One level of nesting gives a 1-D array. Two levels give a 2-D array. The rows must be the same length. Ragged input raises a ValueError rather than guessing, which is the right behaviour because the alternative would be an array of Python objects that looks like an array but performs like a list.
When you need an array of a specific size filled with a known value, np.zeros, np.ones and np.full are faster and clearer than building a list first. np.zeros gives you the array in one allocation. Building a list of zeros and then converting it allocates the list, populates it, then allocates the array and copies, for no benefit.
np.arange and np.linspace generate sequences. arange steps by a fixed increment. linspace divides a range into a fixed number of points. The difference matters when you care about the number of points versus the step size. linspace includes the endpoint by default. arange does not. That is a common source of off-by-one errors.
np.empty allocates memory without filling it. The values are whatever happened to be in memory. It is faster than zeros when you plan to overwrite every element immediately. Using it and then reading before writing is a bug.
This module covers np.array, the convenience constructors, sequence generators, and the dtype argument that overrides inference. The examples show the shape and dtype of each result, because checking those two properties is the first thing you should do after creating an array.
np.array takes a list, a tuple, or nested lists. Infers the shape from the nesting. One level gives you 1-D, two levels gives 2-D, and so on.
Rows must be the same length. Ragged input raises rather than guessing. This Is the right choice. The option would be an array of Python list objects that looks like an array and behaves like nothing you want.
If you truly have ragged data, you want a list of arrays, or padding to a rectangle. You should decide which on purpose.
Worth knowing
np.array infers dimensions from nesting. Ragged rows raise rather than producing something surprising.
zeros, ones, full and eye allocate a known shape in one call. empty does not initialise — only use it when you overwrite everything.
arange takes a step and excludes the stop; linspace takes a count and includes it. Use linspace for floats.
zeros_like and friends copy shape and dtype, which keeps a float32 pipeline from silently widening to float64.
The dtype is fixed at creation. int8 wraps at 127 with no warning, so choose a width that fits your values.
Never grow an array in a loop — np.append copies the whole thing each time. Allocate the result and assign into it.
Creating Arrays
The half-dozen constructors that cover almost all, and which to reach for when.
From a list, and from nested lists
np.array takes what you give it. Nesting decides the dimensions, and ragged input is an error rather than a guess.
example_01.pyNumPy
import numpy as np
a = np.array([1, 2, 3])
b = np.array([[1, 2, 3],
[4, 5, 6]])
print("1-D:", a, "shape", a.shape)
print("2-D:")
print(b)
print(" shape", b.shape, "ndim", b.ndim)
try:
np.array([[1, 2], [3, 4, 5]]) # rows of different length
except ValueError as e:
print()
print("ragged input:", type(e).__name__)
print(" ", str(e).split(".")[0])
Output
Filled arrays of a known shape
Usually you know the shape before the values. These allocate it in one call, which is far better than growing a list and converting.
example_02.pyNumPy
import numpy as np
print("zeros (2,3):"); print(np.zeros((2, 3)))
print()
print("ones (2,3) :"); print(np.ones((2, 3), dtype=int))
print()
print("full :"); print(np.full((2, 3), 7.5))
print()
print("empty : allocated but NOT initialised - contents are whatever")
print(" was in that memory. Use it only when you overwrite it all.")
print()
print("identity :"); print(np.eye(3, dtype=int))
Output
Ranges: arange and linspace
arange takes a step and excludes the stop; linspace takes a count and includes it. That difference is the whole reason both exist.
example_03.pyNumPy
import numpy as np
print("arange(5) :", np.arange(5))
print("arange(2, 10, 3) :", np.arange(2, 10, 3), "<- stop is EXCLUDED")
print()
print("linspace(0, 1, 5) :", np.linspace(0, 1, 5), "<- stop INCLUDED, 5 values")
print("linspace(0, 1, 4) :", np.linspace(0, 1, 4))
print()
print("arange with floats is where people get bitten:")
print(" arange(0, 1, 0.1) length:", len(np.arange(0, 1, 0.1)))
print(" arange(0, 0.3, 0.1) :", np.arange(0, 0.3, 0.1))
print(" floating point makes the count unpredictable - use linspace.")
Output
Like an existing array
The _like family copies shape and dtype, which is how you allocate a result that matches an input.
example_04.pyNumPy
import numpy as np
template = np.array([[1, 2, 3],
[4, 5, 6]], dtype=np.float32)
print("template :", template.shape, template.dtype)
print("zeros_like :", np.zeros_like(template).shape, np.zeros_like(template).dtype)
print("ones_like :", np.ones_like(template).dtype)
print("full_like :"); print(np.full_like(template, 9))
print()
print("Compare with zeros((2,3)), which would silently give you float64")
print("and quietly widen every result that touches it.")
Output
Choosing the dtype up front
The dtype is fixed at creation. Deciding it deliberately avoids both overflow and wasted memory.
example_05.pyNumPy
import numpy as np
small = np.arange(5, dtype=np.int8)
big = np.arange(5, dtype=np.int64)
f32 = np.arange(5, dtype=np.float32)
for name, arr in [("int8", small), ("int64", big), ("float32", f32)]:
print("%-8s %-9s %d bytes each, %d total"
% (name, arr.dtype, arr.itemsize, arr.nbytes))
print()
print("int8 holds -128..127, so it wraps around:")
print(" ", np.array([127], dtype=np.int8) + np.array([1], dtype=np.int8))
print(" no error, no warning - just the wrong number.")
Output
Building one you will fill
The common shape of real code: allocate the result, then write into it. Growing an array in a loop copies it every time.
example_06.pyNumPy
import numpy as np
import time
n = 20_000
# Wrong: every append reallocates and copies the whole thing.
t = time.perf_counter()
grown = np.array([], dtype=np.float64)
for i in range(2000):
grown = np.append(grown, i)
slow = time.perf_counter() - t
# Right: allocate once, then assign.
t = time.perf_counter()
out = np.empty(2000, dtype=np.float64)
for i in range(2000):
out[i] = i
fast = time.perf_counter() - t
print("np.append in a loop : %.4f s" % slow)
print("allocate then fill : %.4f s" % fast)
print("ratio : %.1fx" % (slow / fast))
print("same result :", np.array_equal(grown, out))
print()
print("Better still: np.arange(2000.0) - no Python loop at all.")
Output
When you know the shape but not the values
Most real code knows the shape first. Allocating it right is both faster and clearer than building a list and converting.
np.zeros(shape) and np.ones(shape) are the common ones. np.full(shape, value) fills with anything. np.eye(n) is the identity mat.
Note that the shape arg is a *tuple* for more than one dim: np.zeros((2, 3)). np.zeros(2, 3) is a other function signature and won't do what you meant.
np.empty deserves a warning. It allocates without resetting. The contents are what happened to be in that memory. Not zeros, not anything predictable. It's marginally faster than zeros and only correct when you're about to overwrite each element. Reaching for it by default makes bugs that appear only at times.
arange and linspace
Both make a sequence, and the gap is what you give.
np.arange(start, stop, step) takes a step and excludes the stop, just like Python's range.
np.linspace(start, stop, num) takes a count and includes the stop.
For ints, arange is natural. For floats, it's a trap. The number of elements depends on floating-point build-up. np.arange(0, 1, 0.1) may give you ten elements or eleven based on rounding. np.arange(0, 0.3, 0.1) can include a value slightly above 0.3.
The rule is simple: **if the step is a float, use linspace.** You almost always know how many points you want. linspace gives just that many, with the endpoint where you asked for it.
Matching an existing array
The _like family — zeros_like, ones_like, empty_like, full_like — copies both shape and dtype from an array you already have.
The dtype part is the valuable half. In a pipeline working in float32 for memory reasons, allocating a result with np.zeros(shape) gives float64. The first step that mixes them widens all back. zeros_like keeps the type you chose.
Choosing a dtype
The dtype is fixed at creation and worth choosing rather than accepting.
ints come in int8, int16, int32 and int64, signed and unsigned. Floats come in float32 and float64 (and float16, with real precision costs).
Two reasons to care. Memory: a million float64 values is 8 MB. float32 halves that. This Matters once arrays are large. Overflow: an int8 holds −128 to 127. 127 + 1 gives −128 with no error and no warning. NumPy doesn't promote to a wider type to save you.
That silence is the key part. Python ints grow without limit. NumPy ints wrap. Code ported from lists to arrays can start making wrong numbers rather than raising. This Is much harder to notice.
The default int width is platform-dependent. int64 on most desktops, int32 on this page's 32-bit WebAssembly. Where the values may be large, say what you want.
Never grow an array in a loop
np.append doesn't append. There's nowhere to append to. An array is a fixed block of memory. np.append allocates a new one, copies all across. Returns it. In a loop that is quadratic.
The last editor measures it. The fix is the same as the string-joining fix in Python. Allocate the result once and assign into it, or better, build the whole thing with a vectorised call and no Python loop at all.
If you truly don't know the final size, collect into a Python list and convert once at the end. Lists over-allocate and appending to them is cheap.
Which to reach for
Data in hand: np.array.
Known shape, zeros: np.zeros. Known shape, matching an existing array: zeros_like.
int sequence: np.arange. Float sequence: np.linspace.
A result you're about to fill fully: np.empty, and only then.
Anything else in a loop: stop and ask whether a vectorised formula would build it in one call.
empty is not zeros
np.zeros writes zeros. np.empty allocates and writes nothing. The contents are what was before in that memory.
That makes empty faster, and it's the correct choice when you're about to overwrite each element. It's a bug waiting to happen when you aren't.
The trap is that uninitialised memory is often zeros above all on a freshly started process. Code that accidentally leans on empty giving zeros can pass each test and then give garbage under real load when the allocator hands back recycled memory.
The rule: use empty only when the very next thing you do fills the whole array. If there's any branch where an element might not be written, use zeros.
Bringing data in from elsewhere
np.array on a list is the common case. But It isn't the only door.
np.frombuffer wraps existing bytes without copying. handy for data arriving from a socket, a file read, or another library. The result is read-only if the buffer is. It shares memory with the source. The usual view caveats apply.
np.fromiter builds an array from any iterable, consuming it lazily. This is the one to reach for with a gen. np.array(gen) does not do what you'd hope. It creates a zero-dimensional object array holding the gen itself. That failure is silent and confusing. fromiter with an clear dtype is the fix. Pass count when you know the length. It can preallocate instead of growing inside.
np.loadtxt and np.load cover files, and get a module of their own later.
The structured constructors
np.eye(n) gives an identity mat. np.identity(n) is the same thing with fewer options. np.eye takes a k arg to offset the main-line. This Is how you build shift mats.
np.diag is two functions in one, based on what you hand it. Given a 1-D array it builds a mat with that main-line. Given a 2-D array it extracts the main-line. That overloading is convenient and at times surprising.
np.full(shape, value) fills with a constant. Is clearer than np.ones(shape) * value. It also gets the dtype right, inferring it from the fill value rather than starting from float.
np.tile and np.repeat build larger arrays from smaller ones. They differ in a way worth remembering: tile repeats the whole array, repeat repeats each element in place. np.tile([1,2], 2) gives 1 2 1 2. np.repeat([1,2], 2) gives 1 1 2 2.
array versus asarray
np.array(x) copies by default. np.asarray(x) doesn't copy if x is already an array of the right dtype.
That gap matters at function boundaries. A function that begins a = np.asarray(a) accepts lists and arrays alike. Costs nothing when given an array it can use right. The same function written with np.array copies each call. This Is wasteful in a loop and can be large for large inputs.
Use asarray for "make sure this is an array". Use array when you namely want a copy that the caller can't see you modify.
Note that asarray returning the first means you must not modify it in place unless you own it. The same views-and-copies question that runs through the whole library.
A checklist for choosing
Ask, in order:
Do I already have the values?np.array or np.asarray.
Do I know the shape but not the values?zeros, ones, full, or empty if each element will be overwritten.
Is it a sequence of numbers?arange for a step, linspace for a count. Prefer linspace when the values are floats. arange with a float step has an unpredictable length.
Should it match a thing I have? The _like family. This Copies shape and dtype together and blocks the two from drifting apart.
Is it coming from a gen or a byte buffer?fromiter or frombuffer, never bare np.array.
And in each branch, set the dtype at creation rather than converting later. astype allocates a whole second array. Choosing right the first time avoids both the copy and the class of bugs where an int array silently refuses the float you assign into it.
Common mistakes at this stage
**np.array(gen).** This doesn't build an array from the gen's values. It builds a zero-dimensional object array holding the gen object itself. Each later step behaves bizarrely. np.fromiter(gen, dtype=float) is the correct call. Passing count when the length is known lets it preallocate.
**np.array on ragged input.** Lists of unequal length no longer give an object array silently. Modern NumPy raises. That is an gain. The object array it used to create looked like an array and performed like a list.
**Losing that np.zeros is float.** np.zeros(5) has dtype float64, not int. Code that fills it with counts and then uses it as an index has to convert. The conversion is a copy. np.zeros(5, dtype=int) at creation avoids both.
**np.empty where np.zeros was meant.** Uninitialised memory often holds zeros in a fresh process. This passes tests and fails in production.
**Building with np.append in a loop.** Quadratic. The single most common accidental slowdown in beginner NumPy code.
A note on reproducible construction
Test data and examples gain from being fixed.
np.arange and np.linspace are fixed by build. This Makes them good for examples where the values don't matter but reproducibility does.
Where random data is truly needed, np.random.default_rng(seed) gives a gen whose output is fixed. Seeding at the point of build, rather than leaning on a global, keeps the example self-held. Which is why each random example in this track creates its own gen.
Getting the shape right the first time
Most build bugs are shape bugs, and two habits prevent them.
Pass the shape as a tuple and read it back.np.zeros((3, 4)) is unambiguous. np.zeros(3, 4) is an error. The second spot-based arg is the dtype. A truly confusing failure the first time it happens.
**Use the _like family when an array should match another.** np.zeros_like(a) can't drift out of sync with a the way a hard-coded shape can. When the shape of a changes, the output follows. One fewer place needs editing.
Questions people ask about Creating Arrays
What is the difference between np.array and np.asarray? np.array copies by default, while np.asarray returns the input unchanged if it is already an array of the right dtype. Use asarray in functions that only read their input.
Should I use arange or linspace for floats? Use linspace. With a float step, arange can land one element short or over because of rounding, whereas linspace takes the element count and includes the endpoint.
Is np.empty faster than np.zeros? Slightly, since it skips filling the memory, but it returns whatever bytes were there. Use it only when you will overwrite every element.
Why is growing an array with append in a loop slow? Each append allocates a new array and copies everything. Collect values in a list and convert once, or preallocate with the final shape.
How do I make an array shaped like another one? Use zeros_like, ones_like, empty_like or full_like. They copy the shape and dtype of the template.
What dtype do I get from np.array([1, 2, 3])? An integer type, usually int64 on Linux and macOS. Add one float to the list and every element becomes float64.
A closing note on Creating Arrays
Creation is the cheapest place to prevent problems. The dtype and shape chosen here pass through all that follows.
Setting the dtype at build avoids a copy and a class of silent conversion bugs. Passing the shape as a tuple avoids the confusing failure where the second arg is read as a dtype. And using the _like family keeps derived arrays from drifting out of sync with their source.
Three small habits, applied once, that drop a disproportionate amount of later debugging.
Check yourself
0 of 4
Answer without scrolling back up.
Why prefer `linspace` over `arange` for float steps?
`np.arange(0, 1, 0.1)` may give ten or eleven elements depending on rounding. `linspace` gives exactly the count you asked for, endpoint included.
What does `np.empty((2,3))` contain?
It allocates without initialising. Only correct when you overwrite every element - otherwise it produces bugs that appear only sometimes.
`np.array([127], dtype=np.int8) + 1` gives what?
NumPy integers wrap rather than promoting or raising. Python ints grow without limit, so code ported from lists can start producing wrong numbers instead of errors.
Why is `np.append` in a loop a mistake?
There is nowhere to append to. Allocate the result once and assign into it, or build it with a vectorised call.
Cheat sheet
Creating Arrays
Create NumPy arrays with array, zeros, arange, linspace and random. See which constructor fits each situation and what dtype each one picks.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.