A NumPy empty array comes from np.empty(), which reserves memory without clearing it first:
import numpy as np
a = np.empty(5) # 5 floats of WHATEVER was already in that memory
b = np.empty((2, 3)) # a 2x3 block, same deal
c = np.array([]) # a different thing: an array with zero elements
Those are two separate ideas that share a name. np.empty(5) has five slots full of junk, while np.array([]) genuinely holds nothing.
Timings and output below come from Python 3.12.5, NumPy 2.5.3 on this machine.
What np.empty actually returns
The values are not random. They are whatever was last left in that block of memory, which the screenshot below shows directly:
import numpy as np
# np.empty allocates memory and does NOT clear it
a = np.empty(5)
print("np.empty(5) :", a)
print("dtype :", a.dtype)
# proof that the values are leftovers: dirty some memory, free it, ask for it back
big = np.arange(1000, dtype=np.float64)
del big
recycled = np.empty(1000, dtype=np.float64)
print("\nafter deleting an arange(1000):")
print(recycled[:8])
print("all zeros?", bool((recycled == 0).all()))
Output:
np.empty(5) : [0. 0. 0. 0. 0.]
dtype : float64
after deleting an arange(1000):
[9.64236004e-312 9.63982633e-312 2.00000000e+000 3.00000000e+000
4.00000000e+000 5.00000000e+000 6.00000000e+000 7.00000000e+000]
all zeros? False

arange, ask for the memory back, and you can read the old values.Look at the second array. The numbers 2.0, 3.0, 4.0 and upward are the deleted arange, handed straight back.
The first array in that same run came back as all zeros, because fresh pages from the operating system arrive zeroed. One call leaks, the next looks clean.
That inconsistency is what makes this bug nasty. It works on your machine, passes your tests, and fails in production.
- Use
np.emptyonly when you will overwrite every element immediately. - Use
np.zeroswhen you’ll accumulate into the array, so a missed index shows up as 0 rather than 1e-312. - Use
np.full(shape, np.nan)when a missed index should be obvious and loud.
np.empty vs np.zeros vs np.ones speed
The usual advice is that np.empty is faster because it skips initialisation. That is only true in a narrow range:
import numpy as np
import timeit
print(f"{'size':>10} {'empty':>9} {'zeros':>9} {'ones':>9} microseconds per call")
for n in (100, 10_000, 1_000_000):
reps = 2000 if n <= 10_000 else 200
te = timeit.timeit(lambda: np.empty(n), number=reps) / reps * 1e6
tz = timeit.timeit(lambda: np.zeros(n), number=reps) / reps * 1e6
to = timeit.timeit(lambda: np.ones(n), number=reps) / reps * 1e6
print(f"{n:>10} {te:>9.2f} {tz:>9.2f} {to:>9.2f}")
Output:
size empty zeros ones microseconds per call
100 0.15 0.15 0.75
10000 0.27 1.49 2.09
1000000 11.55 11.30 2010.66

import matplotlib.pyplot as plt
import numpy as np
import timeit
sizes = (100, 10_000, 1_000_000)
rows = {"np.empty": [], "np.zeros": [], "np.ones": []}
for n in sizes:
reps = 2000 if n <= 10_000 else 200
rows["np.empty"].append(timeit.timeit(lambda: np.empty(n), number=reps) / reps * 1e6)
rows["np.zeros"].append(timeit.timeit(lambda: np.zeros(n), number=reps) / reps * 1e6)
rows["np.ones"].append(timeit.timeit(lambda: np.ones(n), number=reps) / reps * 1e6)
x = np.arange(len(sizes))
plt.figure(figsize=(8, 4.5))
for i, (name, colour) in enumerate((("np.empty", "#0b6bcb"), ("np.zeros", "#0a7d32"), ("np.ones", "#c0392b"))):
plt.bar(x + (i - 1) * 0.27, rows[name], 0.26, label=name, color=colour)
plt.yscale("log")
plt.xticks(x, [f"{n:,}" for n in sizes])
plt.xlabel("array size"); plt.ylabel("microseconds (log scale)")
plt.title("np.empty only wins in the middle")
plt.legend(); plt.grid(axis="y", alpha=.3); plt.tight_layout()
plt.show()

np.empty only wins clearly at the middle size.| Array size | Result |
|---|---|
| 100 | All three are effectively identical |
| 10,000 | np.empty is about 5x faster than np.zeros |
| 1,000,000 | np.zeros matches np.empty |
| Any size | np.ones is far slower, up to 100x |
At a million elements the operating system supplies pages that are already zeroed, so np.zeros has nothing left to do.
np.ones has no such shortcut. It must write a 1 into every element, which is why it stays slow at every size.
The practical read: pick np.zeros by default. You only gain from np.empty in the middle of the range, and the safety is worth more than a microsecond.
Creating a zero-length NumPy array
This is the other meaning of empty, and the one people usually want when they’re building an array up:
import numpy as np
# an array with NO elements, which is a different thing from np.empty(5)
a = np.array([])
b = np.empty(0)
c = np.empty((0, 3)) # no rows, but still 3 columns
for name, arr in (("np.array([])", a), ("np.empty(0)", b), ("np.empty((0, 3))", c)):
print(f"{name:<18} shape={str(arr.shape):<8} size={arr.size} dtype={arr.dtype}")
print()
# a zero-length array is the right starting point for vstack, not for indexing
print("np.empty((0, 3)) then vstack:")
stacked = np.vstack([c, [1, 2, 3], [4, 5, 6]])
print(stacked)
Output:
np.array([]) shape=(0,) size=0 dtype=float64
np.empty(0) shape=(0,) size=0 dtype=float64
np.empty((0, 3)) shape=(0, 3) size=0 dtype=float64
np.empty((0, 3)) then vstack:
[[1. 2. 3.]
[4. 5. 6.]]

np.array([]) defaults to float64. If you need integers, say so with np.array([], dtype=int).
np.empty((0, 3)) is the useful one. It gives vstack a correctly shaped starting point, which matters when you build a 2D array row by row.
How to check if a NumPy array is empty
Use .size. Truthiness does not work on arrays and will raise on you:
import numpy as np
empty = np.array([])
full = np.array([1, 2, 3])
# the correct check
print("empty.size == 0 :", empty.size == 0)
print("full.size == 0 :", full.size == 0)
print()
# what happens if you use truthiness instead
for name, arr in (("full", full), ("empty", empty)):
try:
if arr:
pass
print(f"if {name}: -> no error")
except ValueError as err:
print(f"if {name}: -> ValueError: {err}")
Output:
empty.size == 0 : True
full.size == 0 : False
if full: -> ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
if empty: -> ValueError: The truth value of an empty array is ambiguous. Use `array.size > 0` to check that an array is not empty.

A multi-element array raises because the truth value is ambiguous. NumPy cannot guess whether you meant any or all.
An empty array raises too, and the message names the fix directly: Use array.size > 0. There is more on this in checking whether a NumPy array is empty.
Why is np.append in a loop so slow?
np.empty(0) followed by a loop of np.append looks natural if you come from Python lists. It is a trap:
import numpy as np
import time
n = 5000
start = time.perf_counter()
arr = np.empty(0)
for i in range(n):
arr = np.append(arr, i) # allocates a WHOLE NEW array every time
append_time = time.perf_counter() - start
start = time.perf_counter()
arr2 = np.array([i for i in range(n)]) # build a list, convert once
list_time = time.perf_counter() - start
print(f"np.append in a loop : {append_time * 1000:>8.1f} ms")
print(f"list then np.array : {list_time * 1000:>8.2f} ms")
print(f"\nthe loop is {append_time / list_time:.0f}x slower, and it gets worse as n grows")
print("same values?", np.array_equal(arr, arr2))
Output:
np.append in a loop : 10.3 ms
list then np.array : 0.28 ms
the loop is 37x slower, and it gets worse as n grows
same values? True

NumPy arrays have a fixed size. np.append cannot extend one, so it allocates a brand new array and copies everything across, every single iteration.
Build a plain Python list and convert once at the end. Lists over-allocate, so appending to them is cheap.
If you know the final length in advance, allocate with np.empty(n) up front and assign by index. That is the fastest option of all, and it’s the one case np.empty is genuinely made for.
np.empty_like and choosing a dtype
np.empty_like copies both the shape and the dtype of an existing array, which saves repeating yourself:
import numpy as np
template = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int32)
same = np.empty_like(template) # copies shape AND dtype
print("template :", template.shape, template.dtype)
print("empty_like:", same.shape, same.dtype)
print()
# spelling out the dtype yourself
for dt in (np.int8, np.float32, bool):
a = np.empty(3, dtype=dt)
print(f"np.empty(3, dtype={np.dtype(dt).name:<8}) -> itemsize {a.itemsize} bytes")
Output:
template : (2, 3) int32
empty_like: (2, 3) int32
np.empty(3, dtype=int8 ) -> itemsize 1 bytes
np.empty(3, dtype=float32 ) -> itemsize 4 bytes
np.empty(3, dtype=bool ) -> itemsize 1 bytes
