Python LinkedList exercises — reservation number N, repeated members and the queue that never ends (with solutions and a cheat sheet)
In a mock exam I wrote the cycle detector comparing names instead of nodes. I tested it with a normal queue of six members in which Marta appeared twice, and my program answered, very sure of itself, that there was a cycle. There wasn’t: there were two people with the same name. You’ll see it in fragment 3 of “What’s wrong here?”, with the real output.
You’ve seen the theory and practised with three library programs. Now it’s your turn. Three exercises at three levels on the same reservation queue: reading the reservation at a position (from the front and from the back), removing repeated members without creating a new list and detecting whether the queue has a cycle without using extra memory. All three go after the two errors that wreck a LinkedList most often: going one node too far and changing an arrow at the wrong moment.
How to use this article: try each exercise on your own; if you’ve been stuck for more than ten minutes, open the hint; before looking at the solution, do “What’s wrong here?”, and paste your code into Python Tutor to see the arrows. At the end there’s an exam-style quiz and the topic’s cheat sheet.
In all three exercises you add methods to the LinkedList class from the theory (inside the class itself, so self.__first and self.__length work without tricks).
Table of Contents
Python LinkedList exercises: basic level
Exercise 1: get the element at position N
Add to LinkedList a method get_at(position) that returns the value at that position, starting at 0 for the head. If the position doesn’t exist (negative, or greater than or equal to the length), raise IndexError with the message in the output.
Then the challenge: get_from_back(position), where 0 is the last one, 1 the one before last… without using the length and in a single traversal.
With the queue Laura → Pablo → Marta → Iván → Nora, the output must be:
Queue: Laura → Pablo → Marta → Iván → Nora → None get_at(0) → Laura get_at(2) → Marta get_at(4) → Nora get_at(5) → IndexError: position 5 out of range (length 5) get_at(-1) → IndexError: position -1 out of range (length 5) get_from_back(0) → Nora get_from_back(1) → Iván get_from_back(4) → Laura get_from_back(5) → IndexError: position 5 out of range
For get_at, validate before moving and then make position hops with a for. For get_from_back, use two references: move one position + 1 nodes ahead and then move both together until the one in front reaches None. As the gap between them doesn’t change, the one behind ends up exactly where you want.
Python LinkedList exercises: intermediate level
Exercise 2: remove duplicates from a LinkedList
Some members reserve the same book two or three times. Add a method remove_duplicates() that keeps only the first occurrence of each value, changing the list itself (without creating another one) and keeping len correct. It must work with the empty list and with a list without repeats.
Expected output with these four queues:
Before: Laura → Pablo → Laura → Marta → Pablo → Iván → Laura → None (len 7) After: Laura → Pablo → Marta → Iván → None (len 4) Before: Ana → Nora → Iván → None (len 3) After: Ana → Nora → Iván → None (len 3) Before: Marta → Marta → Marta → Marta → None (len 4) After: Marta → None (len 1) Before: None (len 0) After: None (len 0)
A set called seen tells you in O(1) whether a value has already appeared. Traverse with the previous/current pair. The key is when previous moves on: if you delete current, the next node of previous is one you haven’t looked at yet. Try it in your head with four Martas in a row.
Python LinkedList exercises: final challenge
Exercise 3: detect whether a LinkedList has a cycle
A list has a cycle when some node, instead of leading towards None, points back to an earlier one. Traversing it with the usual while current is not None never ends (neither does print(queue) or len(list(queue))).
Write has_cycle(), which returns True or False without changing the list and with O(1) memory: no storing the visited nodes in a set. For testing, also write create_cycle(position), which makes the last node point to the node at that position. With show_steps=True, has_cycle prints each step.
Expected output:
Normal: False With a cycle (Iván → Pablo): step 1: tortoise on Pablo, hare on Marta step 2: tortoise on Marta, hare on Pablo step 3: tortoise on Iván, hare on Iván Result: True Empty: False One node without a cycle: False One node with a cycle: True Six members, Marta twice, no cycle: False
It’s Floyd’s algorithm, the tortoise and the hare. The tortoise moves one node per lap and the hare two. Without a cycle, the hare reaches None. With a cycle, both keep going round and, as the hare gains one node per lap, it ends up catching the tortoise. Compare nodes, not names: look at the last line of the output.
What’s wrong here?
Three fragments with an error of the kind that comes up in exams. Predict what each one prints before reading the explanation.
Fragment 1: the duplicates that don’t go away
remove_duplicates with a single line changed (the one marked WRONG):
def remove_duplicates(self):
"""Keeps only the first occurrence of each value, changing the list itself."""
if self.__first is None: # empty: nothing to do
return
seen = {self.__first.value} # the head is never a duplicate: it is the first
previous = self.__first
current = self.__first.next_node
while current is not None:
if current.value in seen:
previous.next_node = current.next_node # we skip the duplicate
self.__length -= 1
previous = current # WRONG: we also move on when deleting
else:
seen.add(current.value)
previous = current
current = current.next_node
Output:
Before: Laura → Pablo → Laura → Marta → Pablo → Iván → Laura → None (len 7) After: Laura → Pablo → Marta → Iván → None (len 4) Before: Ana → Nora → Iván → None (len 3) After: Ana → Nora → Iván → None (len 3) Before: Marta → Marta → Marta → Marta → None (len 4) After: Marta → Marta → None (len 1) Before: None (len 0) After: None (len 0)
What happens: with the first queue it works, because the repeats aren’t next to each other. With four Martas in a row it leaves Marta → Marta and, on top of that, len says 1. When node 2 is deleted, previous moves on to node 2, which is already out of the chain. When node 3 has to go, the program changes the arrow of node 2, not node 1, and node 1 still points to node 3. __length was decreased three times, but only two nodes really left.
Fix: when you delete, previous doesn’t move. It only moves on when the value is new.
Fragment 2: the position that isn’t validated
get_at without the check at the beginning:
def get_at(self, position):
"""Value at the given position (0 = the head). IndexError if it does not exist."""
current = self.__first
for _ in range(position): # position hops from the head
current = current.next_node
return current.value
Output:
Queue: Laura → Pablo → Marta → Iván → Nora → None
get_at(-1) → Laura
Traceback (most recent call last):
File "get_at_wrong.py", line 110, in <module>
print("get_at(5) →", queue.get_at(5))
~~~~~~~~~~~~^^^
File "get_at_wrong.py", line 87, in get_at
return current.value
^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'value'
What happens: get_at(-1) returns Laura with no error: range(-1) doesn’t do a single lap and stays on the head. It’s the worst kind of bug, because it gives no warning. get_at(5) makes five hops, the last one into None, and then asks for None.value. The message ('NoneType' object has no attribute 'value') doesn’t say what’s wrong; an IndexError with the position does.
Fix: validate position < 0 or position >= self.__length before moving.
Fragment 3: the cycle that doesn’t exist
The detector from my mock exam, comparing values:
def has_cycle(self, show_steps=False):
"""Floyd's algorithm: True if some node points back to an earlier one. O(1) memory."""
tortoise = self.__first
hare = self.__first
step = 0
while hare is not None and hare.next_node is not None:
tortoise = tortoise.next_node # 1 hop
hare = hare.next_node.next_node # 2 hops
step += 1
if show_steps:
print(f" step {step}: tortoise on {tortoise.value}, hare on {hare.value}")
if tortoise.value == hare.value: # WRONG: compares names, not nodes
return True
return False # the hare reached None: there is an end
Last line of the output:
Six members, Marta twice, no cycle: True
What happens: in the queue Laura → Pablo → Marta → Iván → Marta → Nora, at step 2 the tortoise is on the first Marta (position 2) and the hare on the second (position 4). They’re different nodes with the same name, and == says they’re equal: a false positive.
Fix: tortoise is hare. It asks whether they are the same object, which is the only thing that proves the hare has gone round.
Commented solutions
Solution exercise 1
def get_at(self, position):
"""Value at the given position (0 = the head). IndexError if it does not exist."""
if position < 0 or position >= self.__length: # we validate BEFORE moving
raise IndexError(f"position {position} out of range (length {self.__length})")
current = self.__first
for _ in range(position): # position hops from the head
current = current.next_node
return current.value
def get_from_back(self, position):
"""Value counting from the back (0 = the last one), without the length and in one traversal."""
if position < 0:
raise IndexError(f"position {position} out of range")
fast = self.__first
for _ in range(position + 1): # fast goes position + 1 nodes ahead
if fast is None: # the list is shorter than that
raise IndexError(f"position {position} out of range")
fast = fast.next_node
slow = self.__first
while fast is not None: # they move together: the gap does not change
fast = fast.next_node
slow = slow.next_node
return slow.value
get_at validates first and then makes position hops. Validating with the stored length is O(1), and that way there’s no need to check for None inside the loop. Check: get_at(2) hops twice, Laura → Pablo → Marta, and returns Marta.
get_from_back can’t use __length (that’s the challenge), so it uses two references position + 1 nodes apart. Trace of get_from_back(1) (the one before last):
| Moment | fast | slow |
|---|---|---|
| After moving 2 nodes ahead | Marta | Laura |
| Lap 1 | Iván | Pablo |
| Lap 2 | Nora | Marta |
| Lap 3 | None | Iván |
When fast reaches None, slow is on Iván, the one before last. Why position + 1 and not position? Because we want fast to end at None, one position after the last one: if you only move it position nodes ahead, slow ends up one further on. The if fast is None inside the first loop detects that the list is shorter than the position asked for: that’s why get_from_back(5) gives IndexError without ever asking for None.next_node.
The technique of two references at a different distance (or speed) solves many exam exercises: the one before last, the middle one (one moves 1 and the other 2) and exercise 3.
Solution exercise 2
def remove_duplicates(self):
"""Keeps only the first occurrence of each value, changing the list itself."""
if self.__first is None: # empty: nothing to do
return
seen = {self.__first.value} # the head is never a duplicate: it is the first
previous = self.__first
current = self.__first.next_node
while current is not None:
if current.value in seen:
previous.next_node = current.next_node # we skip the duplicate
self.__length -= 1
# previous does NOT move on: we have not looked at its new next node yet
else:
seen.add(current.value)
previous = current
current = current.next_node
The decisions:
- The head goes into
seenbefore the loop. It can never be a repeat (it’s the first occurrence), so the traversal starts at the second node withpreviouson the head. That way there’s no special case of “deleting the head”. - If it deletes,
previousstays still. Its new next node is one we haven’t looked at yet. currentalways moves on, at the end of the loop, in both cases.__lengthgoes down with each deletion, solenkeeps telling the truth.
Trace with four Martas (nodes 1 to 4):
| Lap | previous | current | In seen? | What it does | Chain |
|---|---|---|---|---|---|
| 1 | 1 | 2 | Yes | 1 → 3, previous stays | 1 → 3 → 4 |
| 2 | 1 | 3 | Yes | 1 → 4, previous stays | 1 → 4 |
| 3 | 1 | 4 | Yes | 1 → None | 1 |
Three deletions: 4 − 3 = 1, and len says so. Compare it with the table in fragment 1.
And without a set? For each node, traverse the following ones and delete the equal ones: O(1) memory but O(n²) time. With the set it’s O(n) time and O(n) memory. In the exam, if they don’t forbid auxiliary structures, the set is the way to go.
Solution exercise 3
def has_cycle(self, show_steps=False):
"""Floyd's algorithm: True if some node points back to an earlier one. O(1) memory."""
tortoise = self.__first
hare = self.__first
step = 0
while hare is not None and hare.next_node is not None:
tortoise = tortoise.next_node # 1 hop
hare = hare.next_node.next_node # 2 hops
step += 1
if show_steps:
print(f" step {step}: tortoise on {tortoise.value}, hare on {hare.value}")
if tortoise is hare: # the SAME node, not the same value
return True
return False # the hare reached None: there is an end
def create_cycle(self, position):
"""Only for testing: the last node is made to point to the node at that position."""
target = self.__first
for _ in range(position):
target = target.next_node
last = self.__first
while last.next_node is not None:
last = last.next_node
last.next_node = target
The drawing with the example queue and the table of steps from the output:
The decisions:
- The condition
hare is not None and hare.next_node is not Noneprotects the hare’s two hops. If you only checkedhare, in an even-length listhare.next_node.next_nodewould ask forNone.next_node. Thanks to it, the empty list and the one-node list without a cycle returnFalsewithout any extraif. - First they move, then they’re compared. At the start both are on the head: if you compared before moving them, everything would have a cycle.
isand not==, because of fragment 3.- A node pointing to itself is a cycle of length 1: the tortoise hops to itself, the hare too (twice), and
tortoise is hare.
Why does the hare always catch the tortoise? When both are inside the cycle, on each lap the gap between them shrinks by exactly 1 (the hare takes two steps, the tortoise one). A gap that goes down one at a time ends at 0: it can’t skip it. In the example they meet at step 3, on Iván.
create_cycle only exists for testing: it finds the target node, reaches the last one and hooks it on. Careful: after calling it, don’t print the list: __str__ traverses with __iter__ and would never finish.
Exam-style quiz
Now test yourself as in the lab. The FP2 quizzes look like this: about 10 questions in 10-15 minutes, with single choice, multiple choice (wrong ticks subtract), true or false, fill in the gaps, matching and «what does this code print?». Here are 13 original questions on linked lists. Answer without running anything, press Check and read the explanation for each one. These questions are also the question bank for the FP2 theory mock exam, which will mix them with those from the other topics.
Complete the description of a linked list:
A linked list is made of ____. Each one stores a ____ and a ____ to the next one. The list only needs to know the ____ one, and the last one points to ____.
Words:
- first
- value
- index
- array
- None
- reference
- nodes
Solution
A linked list is made of nodes. Each one stores a value and a reference to the next one. The list only needs to know the first one, and the last one points to None.
It’s a linked structure: objects that refer to each other. A Node object has an attribute (next) that points to another object of its own class.
Comparing a linked list with a Python list (an array), true or false?
- Inserting at the front of a linked list doesn’t depend on how many items it has.
- To reach the item at position i you have to walk through i nodes.
- Accessing an item by position is faster in the linked list than in the array.
- When you delete the first node, all the others have to move one position.
- Without a counter attribute, finding the length means walking the whole list.
Solution
- T — Inserting at the front of a linked list doesn’t depend on how many items it has.
- T — To reach the item at position i you have to walk through i nodes.
- F — Accessing an item by position is faster in the linked list than in the array.
- F — When you delete the first node, all the others have to move one position.
- T — Without a counter attribute, finding the length means walking the whole list.
In the array, my_list[i] is a direct calculation; in the linked list you have to hop along. In exchange, inserting or deleting at the front changes just one reference (in the array everything has to shift).
What does this program print?
class Node:
def __init__(self, value, next=None):
self.value = value
self.next = next
c = Node(3)
b = Node(2, c)
a = Node(1, b)
a.next = c
n = a
text = ""
while n is not None:
text += str(n.value)
n = n.next
print(text)Solution
Answer: 13
At first the chain is 1 → 2 → 3. a.next = c makes the 1 point straight to the 3: node 2 is left out of the chain (that’s how you delete a node from the middle). The walk gives 13.
Which line correctly inserts value at the front of the list?
self.first = Node(value, self.first)self.first = Node(value)self.first.next = Node(value)self.first.next = self.first
Solution
Answer: self.first = Node(value, self.first)
Order matters: the new node has to link to the list before first changes. With Node(value, self.first) both happen in a single line (and it also works with an empty list).
Complete insert_last in a list with _first, _last and _size:
You create new = Node(value). If the list is empty, self._first = self._last = new. If not, the last node links to it with self._last.____ = new and then you update self.____ = new. Finally, self._size ____ 1.
Words:
- _last
- next
- +=
- _first
- -=
- value
Solution
You create new = Node(value). If the list is empty, self._first = self._last = new. If not, the last node links to it with self._last.next = new and then you update self._last = new. Finally, self._size += 1.
Thanks to _last you don’t have to walk the list. Order matters: first link (_last.next = new) and then move _last; the other way round, the new node would point to itself and the chain would break.
What does this program print?
class Node:
def __init__(self, value, next=None):
self.value = value
self.next = next
class Stack:
def __init__(self):
self.first = None
self.size = 0
def push(self, v):
self.first = Node(v, self.first)
self.size += 1
def pop(self):
v = self.first.value
self.first = self.first.next
self.size -= 1
return v
s = Stack()
for x in [5, 8, 1]:
s.push(x)
s.pop()
print(s.first.value, s.size)Solution
Answer: 8 2
Each push inserts at the front, so after 5, 8 and 1 the chain is 1 → 8 → 5. pop removes the first one (the 1). That leaves 8 → 5 and the size drops to 2.
In a singly linked list with _first and _last, what do you need to delete the last node?
- Walk the list up to the second-to-last node, because it’s the one that has to stop pointing to the last
- Nothing special:
self._last = None - Use
del self._last - Walk the list up to the last node
Solution
Answer: Walk the list up to the second-to-last node, because it’s the one that has to stop pointing to the last
That’s why deleting at the end means walking the list even if you have _last. In a doubly linked list (each node with prev) it would be immediate.
insert(i, value) starts with if i < 0 or i > len(self): raise IndexError. Why i > len(self) and not i >= len(self)?
- Because inserting at position
len(self)is valid: it’s appending at the end - Because indexes start at 1
- It’s a bug: it should be
>= - Because that way negative indexes are allowed
Solution
Answer: Because inserting at position len(self) is valid: it’s appending at the end
To read or delete, the last valid position is len - 1; to insert, len is also valid (after the last one).
What does this program print?
class Node:
def __init__(self, value, next=None):
self.value = value
self.next = next
class LinkedList:
def __init__(self, *values):
self.first = None
for v in reversed(values):
self.first = Node(v, self.first)
def __iter__(self):
n = self.first
while n:
yield n.value
n = n.next
l = LinkedList(4, 7, 9)
print(sum(l), max(l), list(l)[1])Solution
Answer: 20 9 7
Inserting at the front in reverse order, the list ends up 4 → 7 → 9. Since __iter__ is a generator, each function (sum, max, list) walks the list from the start: 4 + 7 + 9 = 20, the maximum is 9 and position 1 is 7.
With _first, _last and _size, which operation on a singly linked list still needs to walk it?
- Deleting the last node
- Inserting at the front
- Inserting at the end
- Getting the length
Solution
Answer: Deleting the last node
The extra attributes are there to save walks: _last for inserting at the end, _size for len(). Deleting at the end is the exception in singly linked lists.
When you delete the first node of a list with _first, _last and _size, what do you have to do?
_firstbecomes the second node_sizegoes down by 1- If the list ends up empty, set
_lasttoNone - Walk the nodes to move them one position
- Always change
_last
Solution
Answer: _first becomes the second node · _size goes down by 1 · If the list ends up empty, set _last to None
The case people forget most is the single node: if you don’t set _last = None, the empty list would still hold a reference to the deleted node.
What does this program print?
class Node:
def __init__(self, value, next=None):
self.value = value
self.next = next
def reverse(first):
previous = None
current = first
while current:
nxt = current.next
current.next = previous
previous = current
current = nxt
return previous
p = Node(1, Node(2, Node(3)))
q = reverse(p)
print(q.value, q.next.value, p.next)Solution
Answer: 3 2 None
reverse turns every arrow around: 1 → 2 → 3 becomes 3 → 2 → 1 and it returns the new first node (the 3). p is still node 1, which is now the last one: its next is None.
Why do many implementations define the Node class inside LinkedList?
- Because it’s an internal detail: whoever uses the list works with values, not nodes
- Because Python doesn’t let a class use another one defined outside
- Because the nodes take up less memory that way
- Because walking the list runs faster
Solution
Answer: Because it’s an internal detail: whoever uses the list works with values, not nodes
It’s encapsulation: the public interface is methods like insert, delete or the for. Inside the list it’s used as LinkedList.Node (or self.Node).
Watch it step by step in Python Tutor
Floyd really clicks when you see the two references move. Go to Python Tutor, choose Python 3 and paste the class-free version of exercise 3:
class Node:
def __init__(self, value, next_node=None):
self.value = value # the data: who made the reservation
self.next_node = next_node # reference to the next node (None if it is the last one)
laura, pablo = Node("Laura"), Node("Pablo")
marta, ivan = Node("Marta"), Node("Iván")
laura.next_node = pablo
pablo.next_node = marta
marta.next_node = ivan
ivan.next_node = pablo # the cycle: Iván goes back to Pablo
tortoise = laura
hare = laura
cycle = False
while hare is not None and hare.next_node is not None:
tortoise = tortoise.next_node
hare = hare.next_node.next_node
if tortoise is hare:
cycle = True
break
print("Cycle?", cycle, "| they meet at", tortoise.value)
Output:
Cycle? True | they meet at Iván
What to watch step by step:
- After the four
next_nodeassignments, Iván’s arrow goes back to Pablo: Python Tutor draws the cycle. - On each lap,
tortoisemoves its arrow one node andharetwo. Count the hops. - On the second lap the hare has already gone round the cycle and is behind the tortoise, on Pablo.
- On the third, both arrows reach the same node, Iván:
tortoise is hareisTrue. - Delete the line
ivan.next_node = pabloand run it again: the hare reachesNoneand the result isFalse.
Cheat sheet: Python LinkedList
The essentials in 30 seconds: a node stores a value and a reference to the next one; the list only stores the head. To insert, link first and move afterwards. To delete, you need the previous node. Traverse with while current is not None; to stop at the last one, while current.next_node is not None. Validate before moving and always test the empty list and the one-node list.
| I want to… | Code | Result |
|---|---|---|
| Create the list | queue = LinkedList() | None (empty) |
| Insert at the front | self.__first = self.Node(value, self.__first) | O(1) |
| Insert at the back | while current.next_node is not None: current = current.next_node and current.next_node = new_node | O(n) |
| Traverse | current = current.next_node until None | O(n) |
| Delete the head | self.__first = self.__first.next_node | O(1) |
| Delete another | previous.next_node = current.next_node | O(n) |
| Position N | for _ in range(n): current = current.next_node | O(n) |
| N from the back | Two references n + 1 nodes apart | One traversal |
| Reverse in place | next_node = current.next_node, turn, move on | O(n), O(1) memory |
| Cycle? | Tortoise 1 hop, hare 2, tortoise is hare | O(n), O(1) memory |
| Private attribute from a subclass | self._LinkedList__first | No AttributeError |
Templates for the exercises:
| Exercise | Pattern | Result with the example queue |
|---|---|---|
get_at(2) | Validate + position hops | Marta |
get_from_back(1) | Two references | Iván |
remove_duplicates() | set + previous stays when deleting | Laura → Pablo → Marta → Iván |
has_cycle() | Floyd with is | True with Iván → Pablo |
| Symptom | Likely cause |
|---|---|
'NoneType' object has no attribute 'value' or 'next_node' | You’ve gone one node too far or haven’t validated the empty list |
The list is left with one node and len doesn’t add up | You turned or changed an arrow before saving the next node |
A node repeats endlessly (Ana → Ana → …) | You moved the head before linking the new node |
| Consecutive repeats remain | previous also moves on when you delete |
'MyClass' object has no attribute '_MyClass__first' | You’re using self.__first in a subclass |
| The program doesn’t finish when printing | The list has a cycle (or your insertion created one) |
Before you hand it in, ask yourself:
- Does it work with the empty list and with a single node?
- Is there any
.valueor.next_nodeon something that could beNone? - Have I linked before moving in every change of references?
- Is
lenstill telling the truth after my method?
And with this you close the Python part of FP2. Next comes Java: start with Java syntax, where you’ll see that the references you’ve just mastered work the same way.
Keep goingAll Python articlesDid this article help you? Here is how you can support SergioLearns →




What is an operating system — theory with your laptop on a study afternoon, a write() call and a CPU that never rests (and the three programs that weren’t running at the same time)