Arrays are the substrate of almost every DSA interview, but the tool each language hands you is subtly different: Java gives you a genuinely fixed-size int[] and a dynamic ArrayList; Go splits the same idea into arrays [n]T (values, fixed size) and slices []T (dynamic headers over a backing array); Python's list and JS's Array are always dynamic and store references rather than raw values. This section covers the mechanics of allocating, growing, copying, and mutating these structures correctly and efficiently — not the algorithms you run over them.
Language Verdict: Pros, Cons & Recommendation
list slicing, comprehensions, and sorted() are the most concise way to build and copy arrayssorted() defaults to correct numeric/lexical ordering — no default-comparator surprise like JS- List comprehensions naturally sidestep the row-aliasing bug (
[[0]*m for _ in range(n)]) array module available when you need a packed, fixed-type buffer
- No true fixed-size array —
list is always dynamic and stores pointers, not inline values [[0]*m] * n silently aliases every row to the same list — a classic, easy-to-miss DP-grid bug- Growth factor and resize threshold are CPython implementation details, not spec-guaranteed
int[] is a real fixed-size, contiguous, unboxed array — fastest option, no aliasing risknew int[n][m] allocates independent rows automatically — immune to the multi-dim aliasing bug entirelyArrayList's 1.5x growth factor is documented; pre-sizing with new ArrayList<>(n) skips resize copiesArrays/Collections utility classes cover copy, sort, and range operations in one call each
ArrayList<Integer> boxes every element, adding real pointer-chasing overhead vs. int[]list.remove(1) (by index) vs. list.remove(Integer.valueOf(1)) (by value) overload ambiguity is a genuine footgun- No built-in
enumerate — indexed iteration means manually tracking i in a classic for loop
- Slices store unboxed
int/byte contiguously — Java-int[] locality with Python-list growth via append make([]T, n) / make([]T, n, cap) pre-size in one call; copy/append/slices.Clone cover the usual copies- Arrays
[n]T are real values (assignment copies) when a fixed size is part of the type for i, v := range s is a built-in enumerate with no extra import
- Slicing shares the backing array —
b := a[1:3]; b[0] = 99 mutates a, the classic aliasing bug Python/JS slice-copy avoid append may or may not reallocate; if cap has room, later writes alias the original — a silent cousin of the slice-share bug- No negative indexes or
pop(i) helper — mid-slice delete is a manual append(s[:i], s[i+1:]...)
TypedArrays (Int32Array, etc.) give real fixed-size, contiguous, unboxed storage when neededArray.from({length}, fn) and spread give clean, aliasing-safe ways to build 2D grids- Richest set of indexed-iteration options (
for, .forEach, .entries()) .slice()/.splice() cover copy and mid-array insert/remove in one call each
Array.prototype.sort() defaults to lexicographic ordering — [10, 2, 1].sort() gives [1, 10, 2]push's growth strategy isn't specified by ECMA-262 at all — no guaranteed cost model or pre-sizingArray(n).fill(obj) aliases every slot to the same object reference — the same DP-grid trap as Python.sort() mutates in place *and* returns the same reference, easy to mistake for a non-mutating copy
Recommendation: Python remains the default for speed of writing. Go slices are the nicest array tool of the four when you want unboxed ints without Java's int[] vs ArrayList split — but slicing aliases the backing array, so copy (append([]T(nil), s...) / slices.Clone) when you need independence. In Python and JS, always build 2D grids with a fresh-row-per-iteration idiom, and never trust JS's default .sort() without an explicit comparator.
Coding Mechanics, Side by Side
Java and Go both have a real fixed-size array type (int[], [n]T); Python and JS are dynamic-only, with narrower typed alternatives available. Go's everyday tool is still the slice ([]T), not the array.
# No true fixed-size array type for general use.
nums = [1, 2, 3] # list: always dynamic, heterogeneous-capable
nums.append(4) # grows automatically
# array module: fixed-TYPE (not fixed-size), packed C-style buffer
from array import array
typed = array('i', [1, 2, 3]) # 'i' = C int; still resizable via append
Python has no fixed-size array primitive — list is always dynamic and stores object references, not raw values. The array module gives you a fixed-type, packed buffer (closer to a C array), but it's still dynamically resizable — it constrains element type, not length. For interviews, list is what you reach for essentially every time.
All four give amortized O(1) append, but the real-world memory overhead and growth strategy differ, and only Java's ArrayList growth factor is specified in source as a stable 1.5x.
import sys
lst = []
prev = 0
for i in range(10):
lst.append(i)
size = sys.getsizeof(lst)
if size != prev:
print(len(lst), size) # capacity jumps are visible here
prev = size
CPython over-allocates on resize, roughly newsize + (newsize >> 3) + (3 if newsize < 9 else 6) — not a clean multiplier, but similar in spirit to ~1.125x growth. This is a CPython implementation detail (not part of the language spec), and it only triggers when the backing buffer is actually full. append is amortized O(1) either way.
Array traversal beats linked-list traversal in all four runtimes despite identical Big-O. Java's int[], Go's []int, and JS's TypedArray give you true contiguous data; Python lists (and boxed Java/JS arrays) are contiguous arrays of pointers.
# A Python list is an array of POINTERS to PyObject, not inline values.
lst = [1, 2, 3, 4, 5]
# lst's backing buffer holds 5 pointers (8 bytes each on 64-bit);
# the actual int objects live scattered on the heap.
Traversing a Python list is still faster than a linked list — the pointer array itself is contiguous, so you get decent locality on the pointers — but every element access dereferences to a separately heap-allocated PyObject (boxed int, string, etc.) that can be anywhere in memory. So a Python list is 'contiguous but indirect': strictly better than a linked list, but it does not get the cache-line benefit of a true primitive array.
One of the most common interview bugs: initializing a 2D grid by replicating a row reference instead of creating fresh rows.
# BUG: all rows are the SAME list object
grid = [[0] * 3] * 2
grid[0][0] = 1
print(grid) # [[1, 0, 0], [1, 0, 0]] <- both rows changed!
# FIX: a fresh inner list per row
grid = [[0] * 3 for _ in range(2)]
grid[0][0] = 1
print(grid) # [[1, 0, 0], [0, 0, 0]]
[[0]*m] * n evaluates [0]*m once and replicates the reference n times, so every row is literally the same object — mutating one row mutates all of them. This is one of the most common bugs when initializing a grid for DP or matrix problems. The list-comprehension form calls [0]*m fresh on every iteration, producing n distinct lists.
arr = [1, 2, 3, 4, 5]
sub = arr[1:3] # new list [2, 3], O(k) where k = slice length
sub[0] = 99
print(arr) # [1, 2, 3, 4, 5] unchanged -- sub is a new list
Slicing always allocates a new list and copies the references (a shallow copy) in O(k) time/space for a slice of length k. If the elements themselves are mutable objects, mutating an element (not the container) still affects both lists, since only the container was copied.
Every language has at least one sort that mutates in place — know which is which, and whether it returns a new collection or the same one, before your interview.
arr = [3, 1, 2]
result = arr.sort() # BUG: sorts in place, returns None
print(result) # None
print(arr) # [1, 2, 3] -- arr itself was mutated
new_arr = sorted(arr) # correct: returns a new sorted list
list.sort() mutates in place and returns None — assigning its return value (x = arr.sort()) is a classic bug that silently produces None instead of the sorted list. sorted(iterable) always returns a new list and leaves the original untouched, and it also works on any iterable, not just lists.
for i, val in enumerate(arr):
print(i, val)
for i, val in enumerate(arr, start=1): # custom start offset
print(i, val)
enumerate() is the idiomatic way to get index and value together — cleaner than manual range(len(arr)) indexing, and the start kwarg is handy for 1-indexed output. It returns a lazy iterator, not a list, so it doesn't allocate an extra array.
arr = [1, 2, 3, 4, 5]
arr.pop(1) # O(n): shifts every element after index 1 left
arr.insert(1, 99) # O(n): shifts every element after index 1 right
arr.pop() # O(1): removing from the end is cheap
pop(i) and insert(i, x) are O(n) because CPython physically shifts the underlying buffer (a memmove). pop() with no argument (removes the last element) is O(1) — prefer removing/appending at the end whenever ordering doesn't force you to touch the middle or front.