NumPy Empty Array: np.empty(), Zero-Length Arrays and dtype

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
Command Prompt showing np.empty returning leftover values from a deleted NumPy array instead of zeros
Delete an 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.empty only when you will overwrite every element immediately.
  • Use np.zeros when 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
Command Prompt table comparing the speed of np.empty np.zeros and np.ones at three array sizes
Three sizes, three very different stories.
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()
Bar chart on a log scale comparing np.empty np.zeros and np.ones allocation time at 100, 10000 and 1 million elements
np.empty only wins clearly at the middle size.
Array sizeResult
100All three are effectively identical
10,000np.empty is about 5x faster than np.zeros
1,000,000np.zeros matches np.empty
Any sizenp.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.]]
Command Prompt showing the shape size and dtype of np.array, np.empty zero and np.empty with a zero row count
All three hold nothing. Only the shape differs.

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.
Command Prompt showing ValueError raised when a NumPy array is used in an if statement instead of checking size
Both the full array and the empty one raise, for different reasons.

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
Command Prompt comparing np.append in a loop against building a list and converting it once to a NumPy array
Same result, 28x the time, and the gap widens with size.

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