When I was first learning Python, I came from a background where “array” and “list” were basically interchangeable terms, and it genuinely confused me why Python had a separate array module when lists already seemed to do everything. It took some digging into how Python stores data in memory before it clicked for me why arrays exist as their own distinct thing. In this guide, I want to walk you through everything I wish someone had explained to me at the start.
What Is an Array, Really?
At its core, an array is a data structure that stores a fixed-type collection of values in contiguous memory. That last part — contiguous memory — is the key distinction from a Python list. A list stores references (pointers) to objects that can live anywhere in memory and be of any type. An array, by contrast, stores the actual raw values themselves, laid out one after another, all of the same type.
This matters because it directly affects performance and memory usage. When every element is the same fixed-size type, Python (or any language) can calculate exactly where the Nth element sits in memory using simple arithmetic, and it doesn’t need to store type information for every single element separately.
Python’s Built-In array Module
Python ships with a module simply called array, and it’s the standard way to create true, homogeneous, memory-efficient arrays. I import it like this:
import array
To create an array, I need to specify a typecode, which tells Python what kind of data the array will hold.
numbers = array.array('i', [1, 2, 3, 4, 5])
print(numbers)
Output:
array('i', [1, 2, 3, 4, 5])
Common Typecodes
Here are the typecodes I use most often, along with what they represent:
| Typecode | C Type | Python Type | Minimum Size (bytes) |
|---|---|---|---|
'b' | signed char | int | 1 |
'B' | unsigned char | int | 1 |
'h' | signed short | int | 2 |
'i' | signed int | int | 2 |
'l' | signed long | int | 4 |
'f' | float | float | 4 |
'd' | double | float | 8 |
I always double-check the typecode table in the official documentation when I’m working with a type I don’t use often, since exact byte sizes can vary slightly by platform.
Arrays vs. Lists: Why the Distinction Matters
This was the part that finally made sense of things for me. Consider a Python list:
my_list = [1, "two", 3.0, [4]]
A list can freely mix types because it’s really just a dynamic array of pointers to Python objects. Each object carries its own type information, reference count, and value, wherever it happens to live in memory.
An array.array, on the other hand, can’t do this:
import array
my_array = array.array('i', [1, "two"])
Output:
TypeError: an integer is required (got type str)
Every element must conform to the declared typecode. This restriction is exactly what allows arrays to store raw values directly instead of pointers, which is what makes them more memory-efficient for large collections of uniform numeric data.
Memory Efficiency in Practice
I like to demonstrate this to myself with a quick comparison:
import array
import sys
py_list = list(range(100000))
py_array = array.array('i', range(100000))
print("List size:", sys.getsizeof(py_list))
print("Array size:", sys.getsizeof(py_array))
In my tests, the array consistently uses significantly less memory than the equivalent list, especially as the number of elements grows, because it avoids the per-element pointer and object overhead that a list carries.
Creating Arrays: Different Approaches
import array
# From a list
a1 = array.array('i', [1, 2, 3])
# Empty array, values added later
a2 = array.array('d')
a2.append(1.5)
a2.append(2.5)
# From a range
a3 = array.array('i', range(10))
print(a1, a2, a3)
Output:
array('i', [1, 2, 3]) array('d', [1.5, 2.5]) array('i', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
Basic Array Operations
Arrays support most of the sequence operations I’m used to from lists:
import array
numbers = array.array('i', [10, 20, 30, 40, 50])
print(numbers[0]) # Indexing
print(numbers[-1]) # Negative indexing
print(numbers[1:3]) # Slicing
print(len(numbers)) # Length
print(30 in numbers) # Membership testing
for n in numbers: # Iteration
print(n, end=" ")
Output:
10
50
array('i', [20, 30])
5
True
10 20 30 40 50
Converting Between Arrays and Lists
Since arrays and lists serve different purposes, I convert between them often:
import array
numbers = array.array('i', [1, 2, 3])
# Array to list
as_list = numbers.tolist()
print(as_list, type(as_list))
# List to array
back_to_array = array.array('i', as_list)
print(back_to_array, type(back_to_array))
Output:
[1, 2, 3] <class 'list'>
array('i', [1, 2, 3]) <class 'array.array'>
Arrays and Binary Data
One thing I find genuinely powerful about arrays is how easily they interact with binary data, which makes sense given they mirror C data types so closely:
import array
numbers = array.array('i', [1, 2, 3])
# Convert to raw bytes
raw = numbers.tobytes()
print(raw)
# Convert back from bytes
restored = array.array('i')
restored.frombytes(raw)
print(restored)
Output:
b'\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00'
restored will show: array('i', [1, 2, 3])
This kind of round-tripping is something I use when writing compact binary files or communicating with lower-level systems.
When to Use array Instead of list
I’ve settled into a rule of thumb over the years:
- Use a list when I need to store mixed types, need maximum flexibility, or the collection isn’t especially large.
- Use an array when I’m working with a large amount of homogeneous numeric data and care about memory efficiency, or when I need to interface with binary data or C-level structures.
- Consider NumPy arrays when I need vectorized mathematical operations, multi-dimensional data, or heavy numerical computation — the built-in
arraymodule doesn’t provide that; it’s purely a compact, one-dimensional container.
Common Mistakes Beginners Make
- Assuming array works like a list — Many list methods exist on arrays too, but type enforcement is the big behavioral difference.
- Forgetting the typecode —
array.array()requires a typecode as the first argument; leaving it out raises aTypeError. - Mixing up array with NumPy arrays — They are completely different objects with different capabilities. The built-in
arraymodule is much more limited and doesn’t support multi-dimensional data or math operations likenumpy.ndarraydoes. - Expecting arrays to hold arbitrary objects — They can’t. If you need heterogeneous storage, a list is the right tool.
Real-World Applications
- Embedded and IoT data logging, where memory is limited and readings are uniformly numeric.
- Audio and signal processing, where raw sample data needs a compact representation.
- File format parsing, especially binary formats with fixed-width fields.
- Networking code, where raw byte-level data needs to be packed and unpacked efficiently.
Other Useful Array Methods Worth Knowing
Beyond the basics, a handful of other array methods have proven useful in my day-to-day work:
import array
numbers = array.array('i', [5, 3, 8, 1, 9])
print(numbers.index(8)) # find the index of a value
numbers.remove(3) # remove the first occurrence of a value
print(numbers)
numbers.reverse() # reverse in place
print(numbers)
print(numbers.count(9)) # count occurrences of a value
popped = numbers.pop() # remove and return the last element
print(popped, numbers)
Output:
2
array('i', [5, 8, 1, 9])
array('i', [9, 1, 8, 5])
1
5 array('i', [9, 1, 8])
These methods mirror their list counterparts closely, which makes the transition between the two data structures fairly intuitive once the type-enforcement rule is understood.
Checking Type Information at Runtime
Since arrays track their typecode explicitly, I can inspect it directly, along with the size in bytes of each individual item, which is genuinely useful when debugging memory usage or preparing data for binary interchange:
import array
numbers = array.array('d', [1.1, 2.2, 3.3])
print("Typecode:", numbers.typecode)
print("Item size (bytes):", numbers.itemsize)
print("Total buffer size (bytes):", numbers.itemsize * len(numbers))
Output:
Typecode: d
Item size (bytes): 8
Total buffer size (bytes): 24
I use this kind of introspection whenever I need to calculate exactly how much memory a dataset will consume before loading it, which matters a lot in memory-constrained environments like embedded devices.
FAQs
Is array.array the same as a NumPy array? No. They share a similar spirit (typed, contiguous storage) but NumPy arrays are far more powerful, supporting multi-dimensional data and vectorized math, while the built-in array module is a simpler, one-dimensional container.
Can an array store strings? Not directly as a typecode-supported type for general string data the way a list can — arrays are meant for numeric (and a couple of character-related) C-compatible types.
Are arrays mutable? Yes, arrays are mutable — you can change, append, insert, and delete elements after creation.
Why not just always use lists? Lists are more flexible but less memory-efficient for large amounts of uniform numeric data, and they don’t offer the same direct interoperability with binary formats and C data types.
Summary
Arrays in Python, via the built-in array module, provide a memory-efficient, type-enforced way to store large collections of uniform numeric data in contiguous memory. Understanding the distinction between how lists and arrays are stored internally clarifies why each exists and helps decide which one fits a given task. For most everyday Python work, lists remain the default, but arrays earn their place whenever memory efficiency, binary data handling, or C-level interoperability matters.
