How to Convert Arrays to Lists in NumPy

NumPy arrays are the backbone of numerical computing in Python, but they don't play nicely with everything. You'll inevitably hit situations where you need plain Python lists: serializing data to...

Key Insights

  • The tolist() method is the correct way to convert NumPy arrays to Python lists—it recursively converts all elements to native Python types, not just the outer container.
  • Using list() on a NumPy array only converts the outermost dimension and leaves you with NumPy scalar types inside, which can cause subtle bugs in JSON serialization and type comparisons.
  • Converting arrays to lists has real performance and memory costs; avoid unnecessary conversions by understanding when you actually need native Python types versus when NumPy arrays work fine.

Introduction

NumPy arrays are the backbone of numerical computing in Python, but they don’t play nicely with everything. You’ll inevitably hit situations where you need plain Python lists: serializing data to JSON, passing arrays to libraries that expect native sequences, using list-specific methods like index(), or interfacing with APIs that choke on NumPy types.

The conversion seems trivial—until you discover that the obvious approach doesn’t do what you think it does. This article covers the right way to convert arrays to lists, the wrong way that looks right, and the edge cases that will bite you if you’re not careful.

The tolist() Method

The tolist() method is NumPy’s built-in solution for converting arrays to Python lists. It does two important things: converts the array structure to nested Python lists and converts each element to its corresponding native Python type.

import numpy as np

# Create a simple 1D array
arr = np.array([1, 2, 3, 4, 5])
python_list = arr.tolist()

print(f"Array: {arr}")
print(f"List: {python_list}")
print(f"Array type: {type(arr)}")
print(f"List type: {type(python_list)}")

Output:

Array: [1 2 3 4 5]
List: [1, 2, 3, 4, 5]
Array type: <class 'numpy.ndarray'>
List type: <class 'list'>

The critical detail is what happens to the individual elements:

# Check element types
print(f"Array element type: {type(arr[0])}")
print(f"List element type: {type(python_list[0])}")

# This matters for JSON serialization
import json

# This works
json.dumps(python_list)

# This would fail with numpy.int64
# json.dumps([arr[0]])  # TypeError: Object of type int64 is not JSON serializable

Output:

Array element type: <class 'numpy.int64'>
List element type: <class 'int'>

The tolist() method converts numpy.int64 to Python int, numpy.float64 to Python float, and so on. This recursive type conversion is why tolist() is the correct choice for most use cases.

Converting Multi-Dimensional Arrays

When you call tolist() on multi-dimensional arrays, it creates appropriately nested Python lists. A 2D array becomes a list of lists, a 3D array becomes a list of lists of lists.

# 2D array conversion
arr_2d = np.array([[1, 2, 3], 
                   [4, 5, 6], 
                   [7, 8, 9]])

list_2d = arr_2d.tolist()
print(f"2D array shape: {arr_2d.shape}")
print(f"Converted list: {list_2d}")
print(f"Type of outer list: {type(list_2d)}")
print(f"Type of inner list: {type(list_2d[0])}")
print(f"Type of element: {type(list_2d[0][0])}")

Output:

2D array shape: (3, 3)
Converted list: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Type of outer list: <class 'list'>
Type of inner list: <class 'list'>
Type of element: <class 'int'>

For 3D arrays, the nesting continues:

# 3D array conversion
arr_3d = np.arange(24).reshape(2, 3, 4)
list_3d = arr_3d.tolist()

print(f"3D array shape: {arr_3d.shape}")
print(f"Converted structure:")
for i, layer in enumerate(list_3d):
    print(f"  Layer {i}: {layer}")

Output:

3D array shape: (2, 3, 4)
Converted structure:
  Layer 0: [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]
  Layer 1: [[12, 13, 14, 15], [16, 17, 18, 19], [20, 21, 22, 23]]

The structure maps directly: the first dimension becomes the outer list, subsequent dimensions become progressively nested inner lists.

Alternative: Using list() Constructor

You might think list(arr) does the same thing as arr.tolist(). It doesn’t, and this distinction causes real bugs.

arr = np.array([1, 2, 3, 4, 5])

# Two different approaches
using_list = list(arr)
using_tolist = arr.tolist()

print(f"list(arr): {using_list}")
print(f"arr.tolist(): {using_tolist}")

# They look the same, but check the element types
print(f"\nElement type with list(): {type(using_list[0])}")
print(f"Element type with tolist(): {type(using_tolist[0])}")

Output:

list(arr): [1, 2, 3, 4, 5]
arr.tolist(): [1, 2, 3, 4, 5]

Element type with list(): <class 'numpy.int64'>
Element type with tolist(): <class 'int'>

The list() constructor only converts the outermost container. The elements remain NumPy scalar types. This causes problems:

import json

# tolist() result serializes fine
json.dumps(arr.tolist())  # Works

# list() result fails
try:
    json.dumps(list(arr))
except TypeError as e:
    print(f"JSON error: {e}")

Output:

JSON error: Object of type int64 is not JSON serializable

For multi-dimensional arrays, list() is even more problematic:

arr_2d = np.array([[1, 2], [3, 4]])

using_list_2d = list(arr_2d)
using_tolist_2d = arr_2d.tolist()

print(f"list() result: {using_list_2d}")
print(f"Type of inner element with list(): {type(using_list_2d[0])}")
print(f"Type of inner element with tolist(): {type(using_tolist_2d[0])}")

Output:

list() result: [array([1, 2]), array([3, 4])]
Type of inner element with list(): <class 'numpy.ndarray'>
Type of inner element with tolist(): <class 'list'>

With list(), you get a list containing NumPy arrays—not a nested list structure. Use tolist() unless you specifically want this behavior.

Handling Special Data Types

NumPy supports data types that don’t have direct Python equivalents. Here’s how tolist() handles them.

Datetime Arrays

# datetime64 arrays
dates = np.array(['2024-01-15', '2024-02-20', '2024-03-25'], dtype='datetime64')
dates_list = dates.tolist()

print(f"Original array: {dates}")
print(f"Converted list: {dates_list}")
print(f"Element type: {type(dates_list[0])}")

Output:

Original array: ['2024-01-15' '2024-02-20' '2024-03-25']
Converted list: [datetime.date(2024, 1, 15), datetime.date(2024, 2, 20), datetime.date(2024, 3, 25)]
Element type: <class 'datetime.date'>

NumPy converts datetime64 to Python datetime.date or datetime.datetime depending on the resolution.

Complex Numbers

# Complex arrays
complex_arr = np.array([1+2j, 3+4j, 5+6j])
complex_list = complex_arr.tolist()

print(f"Array element type: {type(complex_arr[0])}")
print(f"List element type: {type(complex_list[0])}")

Output:

Array element type: <class 'numpy.complex128'>
List element type: <class 'complex'>

Structured Arrays

Structured arrays convert to lists of tuples:

# Structured array
dt = np.dtype([('name', 'U10'), ('age', 'i4'), ('score', 'f8')])
structured = np.array([('Alice', 25, 89.5), ('Bob', 30, 92.3)], dtype=dt)

structured_list = structured.tolist()
print(f"Converted: {structured_list}")
print(f"Element type: {type(structured_list[0])}")

Output:

Converted: [('Alice', 25, 89.5), ('Bob', 30, 92.3)]
Element type: <class 'tuple'>

Boolean Arrays

# Boolean arrays
bool_arr = np.array([True, False, True])
bool_list = bool_arr.tolist()

print(f"Array element type: {type(bool_arr[0])}")
print(f"List element type: {type(bool_list[0])}")
print(f"Is Python bool: {bool_list[0] is True}")

Output:

Array element type: <class 'numpy.bool_'>
List element type: <class 'bool'>
Is Python bool: True

Performance Considerations

Converting arrays to lists isn’t free. The operation creates new Python objects for every element, which takes time and memory.

import time

# Timing comparison
sizes = [1000, 10000, 100000, 1000000]

for size in sizes:
    arr = np.random.rand(size)
    
    start = time.perf_counter()
    _ = arr.tolist()
    elapsed = time.perf_counter() - start
    
    print(f"Size {size:>8}: {elapsed*1000:.2f} ms")

Output (approximate, varies by system):

Size     1000: 0.08 ms
Size    10000: 0.65 ms
Size   100000: 6.42 ms
Size  1000000: 64.18 ms

Memory usage also increases significantly:

import sys

arr = np.arange(10000, dtype=np.int64)
lst = arr.tolist()

# Rough memory comparison
arr_size = arr.nbytes
lst_size = sys.getsizeof(lst) + sum(sys.getsizeof(x) for x in lst)

print(f"Array memory: {arr_size:,} bytes")
print(f"List memory (approx): {lst_size:,} bytes")
print(f"Ratio: {lst_size / arr_size:.1f}x")

Output:

Array memory: 80,000 bytes
List memory (approx): 368,872 bytes
Ratio: 4.6x

Python integers are objects with overhead. A NumPy array stores raw values contiguously; a Python list stores references to individual integer objects, each with its own memory overhead.

When to avoid conversion:

  • Processing large datasets where you’ll convert back to arrays anyway
  • Intermediate steps in numerical pipelines
  • When the receiving code can handle NumPy arrays

When conversion is necessary:

  • JSON serialization
  • APIs that explicitly require lists
  • Using list-specific methods (index(), count(), etc.)
  • Interfacing with non-NumPy-aware libraries

Conclusion

Use tolist() for converting NumPy arrays to Python lists. It handles nested structures correctly and converts elements to native Python types, which is what you need for JSON serialization and general interoperability.

Avoid list() for this purpose—it only converts the outer dimension and leaves NumPy types inside, causing subtle bugs that surface at runtime.

For special data types, tolist() does reasonable conversions: datetime64 becomes datetime objects, structured arrays become tuples, and complex numbers become Python complex. Test with your specific data types if you’re unsure.

Finally, remember that conversion has costs. Don’t convert arrays to lists just to iterate over them—NumPy arrays are iterable. Convert when you genuinely need Python lists, and keep data in array form when you don’t.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.