Last modified: Aug 16, 2026
Python Dynamic Array Implementation Guide
Dynamic arrays are a fundamental data structure in Python. Unlike static arrays, they can grow and shrink at runtime. This flexibility makes them essential for many applications.
In this guide, you will learn how to build a dynamic array from scratch. We will cover core concepts like memory allocation, resizing, and common operations. By the end, you will have a solid understanding of how Python's list works internally.
What is a Dynamic Array?
A dynamic array is a resizable array data structure. It provides random access to elements like a static array. However, it can automatically expand when you add more elements than its current capacity.
Python's built-in list is a classic example of a dynamic array. When you append items, the list handles the underlying memory management for you. Understanding this process helps you write more efficient code.
Building your own version clarifies how resizing strategies work. It also shows the trade-offs between speed and memory usage. Let's start with the basic class structure.
Creating the Dynamic Array Class
We will implement a class called DynamicArray. It will use a fixed-size Python list internally to store elements. We also track the current size (number of elements) and capacity (allocated space).
The initial capacity can be small, like 4. We will double this capacity whenever we run out of space. This amortized resizing strategy keeps append operations efficient.
class DynamicArray:
def __init__(self, capacity=4):
"""Initialize the dynamic array with a fixed capacity."""
self.capacity = capacity
self.size = 0
self.data = [None] * capacity # Internal storage
In the __init__ method, we set up the array. The data attribute holds the actual elements. The size attribute counts how many slots are filled.
Implementing Core Operations
Now let's add methods for basic operations. We need append, get, set, and len. These are the building blocks of any array implementation.
The append method adds an element at the end. If the array is full, we must resize it first. The get and set methods access elements by index. The len method returns the current size.
def append(self, element):
"""Add an element to the end of the array."""
if self.size == self.capacity:
self._resize(self.capacity * 2) # Double capacity
self.data[self.size] = element
self.size += 1
def get(self, index):
"""Retrieve element at a given index."""
if index < 0 or index >= self.size:
raise IndexError("Index out of bounds")
return self.data[index]
def set(self, index, element):
"""Replace element at a given index."""
if index < 0 or index >= self.size:
raise IndexError("Index out of bounds")
self.data[index] = element
def __len__(self):
"""Return the number of elements in the array."""
return self.size
Notice the append method checks if the array is full. If so, it calls a private _resize method. We will define that next. This ensures we always have room for new elements.
Resizing the Array
Resizing is the heart of a dynamic array. When the array is full, we allocate a new, larger block of memory. Then we copy all existing elements to the new block.
We typically double the capacity. This strategy ensures that each append operation is O(1) on average. The _resize method handles this task safely.
def _resize(self, new_capacity):
"""Resize the internal storage to a new capacity."""
new_data = [None] * new_capacity
for i in range(self.size):
new_data[i] = self.data[i]
self.data = new_data
self.capacity = new_capacity
The _resize method creates a new list with the desired capacity. It then copies each element from the old list. Finally, it updates the data reference and the capacity attribute.
This operation is O(n) because we copy n elements. However, it happens rarely. When it does, it doubles the space, so future appends are fast. This is called amortized analysis.
Adding More Useful Methods
Beyond the basics, we can add methods for insertion and deletion. The insert method adds an element at a specific position. The pop method removes and returns the last element.
These operations require shifting elements. Insertion shifts elements to the right. Deletion shifts elements to the left. Let's implement them carefully.
def insert(self, index, element):
"""Insert an element at a specific index."""
if index < 0 or index > self.size:
raise IndexError("Index out of bounds")
if self.size == self.capacity:
self._resize(self.capacity * 2)
# Shift elements to the right
for i in range(self.size, index, -1):
self.data[i] = self.data[i - 1]
self.data[index] = element
self.size += 1
def pop(self):
"""Remove and return the last element."""
if self.size == 0:
raise IndexError("Pop from empty array")
element = self.data[self.size - 1]
self.data[self.size - 1] = None # Clear reference
self.size -= 1
# Shrink if too empty (optional)
if self.size < self.capacity // 4:
self._resize(self.capacity // 2)
return element
The insert method first checks bounds and resizes if needed. Then it shifts elements from the end to the target index. Finally, it places the new element and increments the size.
The pop method removes the last element. It also clears the old reference to help garbage collection. Optionally, we shrink the array when it's too empty to save memory.
Testing the Implementation
Let's test our dynamic array with a simple example. We will append several elements and observe how the capacity grows. Then we will try insert and pop operations.
# Create a dynamic array
arr = DynamicArray()
print(f"Initial size: {len(arr)}, capacity: {arr.capacity}")
# Append elements
for i in range(1, 7):
arr.append(i * 10)
print(f"After append {i}: size={len(arr)}, capacity={arr.capacity}")
# Access elements
print("Element at index 2:", arr.get(2))
# Insert at position 1
arr.insert(1, 99)
print("After insert:", [arr.get(i) for i in range(len(arr))])
# Pop last element
last = arr.pop()
print("Popped:", last)
print("Final array:", [arr.get(i) for i in range(len(arr))])
Run this code to see how the array behaves. You will notice the capacity doubles when needed. This demonstrates the resizing strategy in action.
Initial size: 0, capacity: 4
After append 1: size=1, capacity=4
After append 2: size=2, capacity=4
After append 3: size=3, capacity=4
After append 4: size=4, capacity=4
After append 5: size=5, capacity=8
After append 6: size=6, capacity=8
Element at index 2: 30
After insert: [10, 99, 20, 30, 40, 50, 60]
Popped: 60
Final array: [10, 99, 20, 30, 40, 50]
As you can see, the array grows from capacity 4 to 8 on the fifth append. This is the dynamic behavior we wanted. The insert and pop operations work correctly too.
Performance Considerations
Dynamic arrays offer O(1) average time for append and pop operations. However, worst-case resizing is O(n). This is acceptable because resizing happens infrequently.
Accessing an element by index is always O(1). This makes dynamic arrays excellent for random access. For insertion and deletion at arbitrary positions, they are O(n) due to shifting.
If you need frequent insertions at the beginning, consider using a deque or linked list. For most use cases, a dynamic array is the right choice. Python's list is highly optimized for this purpose.
Comparing with Python's Built-in List
Our implementation is a simplified version of Python's list. The built-in list uses a similar resizing strategy. However, it is written in C and heavily optimized for performance.
Python's list also supports slicing, iteration, and many other methods. Our custom class focuses on the core mechanics. This helps you understand what happens under the hood.
To learn more about how Python allocates memory for arrays, check out our Python Array Memory Allocation Explained guide. It dives deeper into the memory management aspects.
Common Use Cases
Dynamic arrays are everywhere in Python. They are used for stacks, queues, and as general-purpose sequences. Understanding their behavior helps you choose the right data structure.
For example, you can use a dynamic array to implement a stack. Append and pop operations at the end are perfect for LIFO behavior. Similarly, you can build a queue using two arrays.
Our custom implementation is educational. In real projects, you should always use Python's built-in list. It is faster, safer, and more feature-rich. But knowing the internals makes you a better programmer.
Edge Cases and Error Handling
Our implementation handles several edge cases. We raise IndexError for invalid indices. We also check for empty array pops. These safeguards prevent silent bugs.
One thing to note is the shrink logic in pop. We only shrink when the size drops below a quarter of the capacity. This prevents frequent resizing when the array oscillates around a threshold.
You can adjust these thresholds based on your needs. For instance, a smaller shrink threshold saves more memory but may cause more resizing. A larger threshold wastes memory but reduces resizing overhead.
Conclusion
In this guide, you learned how to implement a dynamic array in Python. We covered the class structure, core operations, and resizing logic. You also saw how to test and optimize the implementation.
Dynamic arrays are a powerful tool in any programmer's toolkit. They combine the speed of arrays with the flexibility of dynamic resizing. Now you know exactly how they work.
To further enhance your understanding, explore related topics like array type casting and iteration. Read our Python Array Type Casting Guide to learn about converting data types. Also, check out Python Array Unpacking: A Clear Guide for advanced techniques.
Keep practicing by adding more methods like remove or clear. This will solidify your knowledge. Happy coding!