054. Lists
Ordered mutable collections of arbitrary items
054. Lists
Aryan’s RAM monitor accumulates process records as it scans. He needs a list that grows, shrinks, and stays ordered by discovery time:
processes = [] # start empty
for proc in psutil.process_iter(['pid', 'name', 'memory_info']):
try:
rss_mb = proc.info['memory_info'].rss / 1e6
processes.append({ # add to end — O(1)
"name": proc.info['name'],
"pid": proc.info['pid'],
"rss_mb": rss_mb,
})
except (psutil.NoSuchProcess, psutil.AccessDenied):
passThen he pops the highest-memory process off a sorted copy to alert on it, without destroying the original list:
ranked = sorted(processes, key=lambda p: p["rss_mb"], reverse=True)
top = ranked[0] # highest RSS — read only, no mutation
print(f"Top consumer: {top['name']} ({top['rss_mb']:.0f} MB)")The trap he falls into first: processes + [new_proc] looks like it adds to the list, but it creates a new list and discards it. .append() is the mutating version — it modifies processes in place.
💡 Fun fact: Python lists are implemented as dynamic arrays — contiguous blocks of memory that double in capacity when full. This means append() is amortized O(1): most appends are instant, but occasionally Python allocates a new, larger array and copies everything over. This “doubling” strategy (used in C++’s std::vector and Java’s ArrayList too) was invented by computer scientist John L. Bentley and ensures that building a list of n items takes O(n) time total.
⚠️ Watch out: lst + [item] vs lst.append(item) is one of the most common beginner traps in Python. lst + [item] creates a new list and the original lst is unchanged — if you write lst + [item] without assigning the result, the new item is silently discarded. Always use .append() when you want to mutate the list in place.
🤔 Think about it: Lists are passed by reference in Python — add_song(playlist, song) modifies the caller’s list, not a copy. If you wanted add_song to return a new list without touching the original, how would you implement that? And why might a “pure” function that never mutates its inputs be preferable in some situations?
Learning objectives
- Create lists and add/remove elements
- Use append() for adding and pop() for removing
- Access first/last elements with [0] and [-1]
Key concepts
- list
- append()
- pop()
- mutable
Try it
Concept detail
Lists are ordered, mutable, can hold any type, and can grow/shrink. Create: [], [1,2,3], list(“abc”) == [‘a’,‘b’,‘c’], list(range(5)).
Mutation vs. creation — this trips everyone up: lst.append(x) # mutates lst — adds x to the end, returns None lst + [x] # creates a NEW list — lst is unchanged lst.extend([x,y]) # mutates lst — adds multiple items lst += [x] # mutates lst in place (same as extend for lists)
Other common methods: pop() — remove and return last element pop(0) — remove and return first element (O(n) — slow for big lists) insert(i, x) — insert x at index i remove(x) — remove first occurrence of x
Lists are passed by reference — mutating inside a function affects the caller. Use lst.copy() to avoid mutating the caller’s list.
Solution
def create_playlist(songs):
return list(songs)
def add_song(playlist, song):
playlist.append(song)
return playlist
def remove_first(playlist):
return playlist.pop(0)
def playlist_info(playlist):
return {"count": len(playlist), "first": playlist[0], "last": playlist[-1]}Tests
def test_create_playlist():
result = create_playlist(["song1", "song2"])
assert type(result) == list
assert result == ["song1", "song2"]
def test_add_song():
pl = ["a", "b"]
result = add_song(pl, "c")
assert result == ["a", "b", "c"]
assert pl == ["a", "b", "c"] # mutated in place
def test_add_song_empty():
pl = []
add_song(pl, "x")
assert pl == ["x"]
def test_remove_first():
pl = ["a", "b", "c"]
removed = remove_first(pl)
assert removed == "a"
assert pl == ["b", "c"]
def test_playlist_info():
info = playlist_info(["rock", "jazz", "pop"])
assert info["count"] == 3
assert info["first"] == "rock"
assert info["last"] == "pop"