Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Implement a singly linked list in Python with a Node object that stores a value and a reference to the next node, plus a list object that tracks its head. Keep a tail when you need constant-time appends and a size counter when constant-time length checks matter. Traversal, searching and index lookup remain O(n), so Python’s built-in list or collections.deque is usually a better production choice unless you specifically need node links.

The data model: nodes and links

A singly linked list is a chain of nodes. Each node contains a payload and one reference, conventionally called next. The final node points to None. The container keeps the first node in head; retaining tail avoids walking the chain for every append.

class Node:
    def __init__(self, value, next_node=None):
        self.value = value
        self.next = next_node

class LinkedList:
    def __init__(self):
        self.head = None
        self.tail = None
        self.size = 0

An empty list has head is None and tail is None. For a non-empty list, head and tail refer to nodes in the same chain, and tail.next must be None. These are the core invariants your methods must preserve.

A complete singly linked-list implementation

The following implementation supports append, prepend, search, indexed lookup, deletion by value, deletion after a known predecessor, iteration and length. It uses a documented policy for empty operations: lookup and deletion return None or False rather than raising an accidental attribute error.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#1 Best Overall
Sale
Data Structures and Algorithms in Python
  • Used Book in Good Condition
class Node:
    def __init__(self, value, next_node=None):
        self.value = value
        self.next = next_node

    def __repr__(self):
        return f"Node({self.value!r})"


class LinkedList:
    def __init__(self):
        self.head = None
        self.tail = None
        self.size = 0

    def __len__(self):
        return self.size

    def append(self, value):
        """Add value at the end in O(1) time."""
        node = Node(value)
        if self.head is None:
            self.head = self.tail = node
        else:
            self.tail.next = node
            self.tail = node
        self.size += 1

    def prepend(self, value):
        """Add value at the beginning in O(1) time."""
        node = Node(value, self.head)
        self.head = node
        if self.tail is None:
            self.tail = node
        self.size += 1

    def find(self, value):
        """Return the first matching Node, or None."""
        current = self.head
        while current is not None:
            if current.value == value:
                return current
            current = current.next
        return None

    def get(self, index):
        """Return the value at index; raise IndexError if absent."""
        if index < 0:
            raise IndexError("negative indexes are not supported")
        current = self.head
        for _ in range(index):
            if current is None:
                raise IndexError("linked-list index out of range")
            current = current.next
        if current is None:
            raise IndexError("linked-list index out of range")
        return current.value

    def remove_first(self, value):
        """Remove the first matching value and return True if removed."""
        previous = None
        current = self.head
        while current is not None:
            if current.value == value:
                if previous is None:
                    self.head = current.next
                else:
                    previous.next = current.next
                if current is self.tail:
                    self.tail = previous
                self.size -= 1
                if self.size == 0:
                    self.head = self.tail = None
                return True
            previous, current = current, current.next
        return False

    def remove_after(self, predecessor):
        """Remove predecessor.next; return the removed value or None."""
        if predecessor is None or predecessor.next is None:
            return None
        removed = predecessor.next
        predecessor.next = removed.next
        if removed is self.tail:
            self.tail = predecessor
        self.size -= 1
        return removed.value

    def pop_front(self):
        """Remove and return the first value, or None when empty."""
        if self.head is None:
            return None
        value = self.head.value
        self.head = self.head.next
        self.size -= 1
        if self.head is None:
            self.tail = None
        return value

    def __iter__(self):
        current = self.head
        while current is not None:
            yield current.value
            current = current.next

    def __repr__(self):
        return "LinkedList([" + ", ".join(repr(x) for x in self) + "])"


if __name__ == "__main__":
    numbers = LinkedList()
    numbers.append(2)
    numbers.prepend(1)
    numbers.append(3)
    print(numbers)                 # LinkedList([1, 2, 3])
    print(numbers.get(1))          # 2
    print(numbers.find(3))         # Node(3)
    print(numbers.remove_first(2)) # True
    print(list(numbers))           # [1, 3]

Appending and prepending

append links a new node from the old tail and then moves tail. The first append initializes both endpoints. prepend points the new node at the old head and moves head; if the list was empty, it also initializes tail.

Traversal and search

Starting at head, follow current.next until the reference is None. find returns the node, which is useful when a later operation already has a node reference. Returning the node rather than only a Boolean avoids a second traversal.

Deletion details

To remove a node from a singly linked list, the predecessor must skip it by assigning predecessor.next = removed.next. Removing the head is a special case because there is no predecessor. Removing the tail requires moving tail to the predecessor. If the final node is removed, both endpoints must become None.

Complexity: linked list versus Python containers

The table assumes a singly linked list that stores both head and tail. “Remove after predecessor” is constant time only when that predecessor node is already known; finding it is linear.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Operation/design Singly linked list (head and tail) Python list collections.deque
Indexing O(n) traversal O(1) O(1) at ends; slower in the middle
Prepend O(1) O(n), because elements shift Approximately O(1) with appendleft
Append O(1) with tail; O(n) without it Amortized O(1) Approximately O(1)
Search O(n) O(n) O(n)
Remove after known predecessor O(1) Usually O(n) due to shifting Endpoint operations are approximately O(1)

Python’s lists are variable-length arrays backed by a contiguous array of references, so indexing does not depend on list size. The Python tutorial recommends collections.deque for queues; its documentation describes approximately O(1) performance for appends and pops at either end, while indexing in the middle is slower.

Choosing the right structure

Use a custom linked list when links are the lesson or the requirement

  • You are learning pointers-by-reference, invariants and node-based algorithms.
  • An algorithm already holds node references and must splice nodes without shifting an array.
  • You need a specialized structure whose semantics are not supplied by standard containers.

Use Python list for indexed, compact sequences

Choose a list for random access, compact storage and cache-friendly iteration. A custom linked list creates a separate Python object for every node and adds a reference hop for every element, so it can use more memory and iterate less efficiently than a contiguous list.

Use deque for queues, stacks and double-ended workloads

For production code that repeatedly adds or removes at both ends, import deque and use append, appendleft, pop and popleft. It supplies the operation profile most application queues need without maintaining your own pointer invariants.

Testing the edge cases that break linked lists

Test transitions, not just a long happy-path sequence:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  1. Start empty and verify head, tail and len(list).
  2. Append one value; confirm head is tail and tail.next is None.
  3. Prepend to a one-node list and append after prepending.
  4. Remove the head, then remove the tail, then remove the only remaining node.
  5. Search for a missing value and for duplicate values; confirm only the first duplicate is removed.
  6. Call get with zero, the last valid index and an out-of-range index.
  7. Repeat append/remove operations and check that size equals the number produced by list(linked_list).
def check_invariants(linked):
    values = list(linked)
    assert len(linked) == len(values)
    assert (linked.head is None) == (len(values) == 0)
    assert (linked.tail is None) == (len(values) == 0)
    if linked.tail is not None:
        assert linked.tail.next is None
        assert linked.find(values[-1]) is not None


items = LinkedList()
check_invariants(items)
items.append("a")
check_invariants(items)
items.prepend("b")
items.append("a")
assert items.remove_first("a")
assert list(items) == ["b", "a"]
assert items.remove_first("missing") is False
check_invariants(items)

Troubleshooting common implementation failures

Append raises an attribute error on an empty list

Cause: the method writes to self.tail.next before initializing tail. Fix the empty branch first, assigning self.head = self.tail = node.

The tail still points to a deleted node

Cause: deletion updates the predecessor but not the endpoint. When the removed node is tail, assign tail = predecessor; when the list becomes empty, clear both endpoints.

Iteration never ends

Cause: a link points backward or to an earlier node, creating a cycle. Assign each next exactly once during ordinary insertion and assert tail.next is None. If cycles are valid for your algorithm, a normal terminating iterator needs a separate visited-node policy.

The reported length disagrees with iteration

Cause: an early return skipped size += 1 or size -= 1, or a caller changed links directly. Keep structural changes inside methods and test the size after every mutation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Indexing is unexpectedly slow

Cause: every lookup walks from head. A linked list cannot provide array-style random access without an additional index structure; use a Python list when frequent indexing is central.

Performance, memory and reliability considerations

Keeping tail changes append from a full traversal to O(1), but it does not improve search or indexing. Keeping size makes len O(1) at the cost of maintaining another invariant. If you do not need those operations, a smaller implementation can omit the fields, accepting the corresponding trade-offs.

Node objects carry Python-object and reference overhead. For large homogeneous data, a built-in list is generally more compact; for endpoint queues, deque avoids custom pointer maintenance. A linked list is reliable only when all mutation paths handle the empty, one-node, head and tail cases consistently.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If you need screenshots of documentation, demos or a rendered linked-list visual rather than a browser automation script, ScreenshotNeo returns an image or PDF from one GET request. Its cleanup steps accept cookie and consent banners like a visitor, then remove more than 60 known consent platforms, newsletter popups and chat widgets; each step can be disabled. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits cost nothing, and response headers report X-Page-Verdict and X-Billed.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Example using the documented API (see the ScreenshotNeo documentation):

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

It also provides an MCP server with take_screenshot, get_page_info and capture_pdf for Claude, Cursor and other MCP clients. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Can a linked list support duplicate values?

Yes. Values are compared with ==; the sample’s removal method intentionally removes only the first matching node.

Why does the sample reject negative indexes?

Negative-index behavior is a policy choice. The implementation raises IndexError so it does not silently diverge from the documented traversal contract; you can add explicit translation from the end if your API requires it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Is a doubly linked list faster?

It can move in both directions and delete a node when its neighboring references are available, but every node needs an additional prev reference and every mutation must maintain more links. Use it only when backward traversal or that deletion pattern justifies the extra overhead.

Frequently Asked Questions

Can a linked list support duplicate values?

Yes. Values are compared with ==; the sample’s removal method removes only the first matching node.

Why does the sample reject negative indexes?

Negative-index behavior is a policy choice. This implementation raises IndexError; add explicit translation from the end only if your API requires it.

Is a doubly linked list faster?

It supports backward traversal and can delete with neighboring references, but adds a prev reference and more invariants.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The Bottom Line

Build a custom linked list to understand or exploit node links; otherwise prefer Python list for indexed sequences and collections.deque for fast operations at both ends.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.