Singly and Doubly Linked Lists in Python, Built From Scratch

A linked list stores each value in its own node, with the nodes connected by pointers instead of sitting in one contiguous block like a Python list. This post builds a SinglyLinkedList (each node points only forward) and a DoublyLinkedList (each node points both forward and backward), then runs insert/delete operations on each and prints the resulting order.

The code

class Node:
    # Singly linked node: value + pointer to the next node (or None)
    def __init__(self, value):
        self.value = value
        self.next = None


class SinglyLinkedList:
    def __init__(self):
        self.head = None

    def insert_front(self, value):
        # O(1): new node just points at the old head
        node = Node(value)
        node.next = self.head
        self.head = node

    def insert_end(self, value):
        # O(n): must walk to the last node first, no tail pointer here
        node = Node(value)
        if self.head is None:
            self.head = node
            return
        current = self.head
        while current.next is not None:
            current = current.next
        current.next = node

    def delete(self, value):
        # O(n): find the node, relink its predecessor to its successor
        if self.head is None:
            return
        if self.head.value == value:
            self.head = self.head.next
            return
        current = self.head
        while current.next is not None and current.next.value != value:
            current = current.next
        if current.next is not None:
            current.next = current.next.next

    def to_list(self):
        result = []
        current = self.head
        while current is not None:
            result.append(current.value)
            current = current.next
        return result


class DNode:
    # Doubly linked node: value + pointers to both neighbors
    def __init__(self, value):
        self.value = value
        self.prev = None
        self.next = None


class DoublyLinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

    def insert_front(self, value):
        node = DNode(value)
        node.next = self.head
        if self.head is not None:
            self.head.prev = node
        self.head = node
        if self.tail is None:
            self.tail = node

    def insert_end(self, value):
        # O(1): tail pointer means no walk needed, unlike the singly linked version
        node = DNode(value)
        node.prev = self.tail
        if self.tail is not None:
            self.tail.next = node
        self.tail = node
        if self.head is None:
            self.head = node

    def to_list_forward(self):
        result = []
        current = self.head
        while current is not None:
            result.append(current.value)
            current = current.next
        return result

    def to_list_backward(self):
        result = []
        current = self.tail
        while current is not None:
            result.append(current.value)
            current = current.prev
        return result


print("=== SinglyLinkedList ===")
sll = SinglyLinkedList()
sll.insert_front(3)
sll.insert_front(2)
sll.insert_front(1)
print("after insert_front(3), insert_front(2), insert_front(1):", sll.to_list())
sll.insert_end(99)
print("after insert_end(99):", sll.to_list())
sll.delete(2)
print("after delete(2):", sll.to_list())

print("\n=== DoublyLinkedList ===")
dll = DoublyLinkedList()
dll.insert_end(10)
dll.insert_end(20)
dll.insert_end(30)
print("after insert_end(10), insert_end(20), insert_end(30):", dll.to_list_forward())
dll.insert_front(1)
print("after insert_front(1):", dll.to_list_forward())
print("same list walked backward from tail:", dll.to_list_backward())

SinglyLinkedList tracks only head; insert_end has to walk the whole chain to find the last node. DoublyLinkedList tracks both head and tail, so insert_end attaches directly at tail without walking, and the list can be traversed in either direction via next/prev.

Running it

Real output:

=== SinglyLinkedList ===
after insert_front(3), insert_front(2), insert_front(1): [1, 2, 3]
after insert_end(99): [1, 2, 3, 99]
after delete(2): [1, 3, 99]

=== DoublyLinkedList ===
after insert_end(10), insert_end(20), insert_end(30): [10, 20, 30]
after insert_front(1): [1, 10, 20, 30]
same list walked backward from tail: [30, 20, 10, 1]

On the singly linked list, three insert_front calls with 3, 2, 1 produced [1, 2, 3] — each new value lands at the head, reversing the insertion order. insert_end(99) appended after walking to the tail, giving [1, 2, 3, 99], and delete(2) removed the middle node, leaving [1, 3, 99]. On the doubly linked list, three insert_end calls kept insertion order ([10, 20, 30]) since each attaches directly at tail; insert_front(1) put 1 at the head; and walking backward from tail via prev reproduced the same four values in reverse, [30, 20, 10, 1].

Takeaway

The run showed the structural difference in practice: the singly linked list’s insert_front reverses insertion order while insert_end requires a full walk, and the doubly linked list’s tail pointer let insert_end attach directly while prev pointers made a full reverse traversal possible.