Before I could ever build a full linked list, queue, or tree in Python, I had to understand the humble building block that makes all of them possible: the node. It seems almost too simple to deserve its own article, but I’ve found that truly understanding how a node works — how it holds data, how it references the next node, and how memory is actually allocated for it — is what makes everything built on top of it click. In this guide, I’m going to walk through writing a simple linked list node in Python from scratch, and explain the reasoning behind every design decision.
What Is a Node, Conceptually?
A node is the smallest unit of a linked list. It’s a small container that holds two things:
- Data — the actual value I want to store.
- A reference (pointer) to the next node in the sequence, or
Noneif it’s the last node.
Unlike arrays, where elements are stored in contiguous memory and accessed by index, a linked list is a chain of these nodes scattered across memory, connected only by references. This is a fundamentally different way of organizing data, and it comes with its own trade-offs that I’ll get into later.
Writing My First Node Class
The most natural way to represent a node in Python is with a class:
class Node:
def __init__(self, data):
self.data = data
self.next = None
That’s genuinely it. This tiny class is the foundation of every linked list, stack, and queue I’ve ever built in Python.
Let me create a few nodes and connect them manually to see how this works:
node1 = Node(10)
node2 = Node(20)
node3 = Node(30)
node1.next = node2
node2.next = node3
current = node1
while current is not None:
print(current.data)
current = current.next
Output:
10
20
30
This tiny loop is essentially a manual traversal of a linked list — I start at node1, print its data, then move to whatever next points to, repeating until I hit None.
Adding a Useful repr Method
When I’m debugging, printing a Node object directly isn’t very informative:
print(node1)
Output:
<__main__.Node object at 0x7f8a2d3f4a90>
I usually add a __repr__ method so debugging is easier:
class Node:
def __init__(self, data):
self.data = data
self.next = None
def __repr__(self):
return f"Node({self.data!r})"
node1 = Node(10)
print(node1)
Output:
Node(10)
This one small addition has saved me a lot of confusion when inspecting linked list structures in a debugger or console session.
Making the Node Generic and Type-Safe
For larger projects, I like adding type hints to make the code more maintainable and to let tools like mypy catch mistakes early:
from typing import Optional, Any
class Node:
def __init__(self, data: Any) -> None:
self.data: Any = data
self.next: Optional["Node"] = None
def __repr__(self) -> str:
return f"Node({self.data!r})"
Using Optional["Node"] (with a forward reference in quotes) is necessary here because at the point the class body is being defined, Node itself doesn’t exist yet as a completed type — Python resolves string-based forward references lazily.
Internal Working: How Python Represents a Node in Memory
Every instance of Node is a full Python object, which means it carries some overhead beyond just the two attributes I defined. Under CPython, each object instance has:
- A reference count (used for garbage collection).
- A pointer to its type object.
- A
__dict__(unless I use__slots__) that stores instance attributes as key-value pairs.
This means a naive Node object is heavier in memory than, say, a raw C struct with two fields would be. I can inspect this with the sys module:
import sys
node = Node(10)
print(sys.getsizeof(node))
The exact number varies between Python versions, but it’s noticeably larger than the size of a simple integer, because of that extra object bookkeeping.
Reducing Memory Overhead with slots
If I’m building a linked list with potentially millions of nodes — say, for a large in-memory dataset — I can significantly reduce memory usage by using __slots__, which tells Python not to create a __dict__ for each instance:
class Node:
__slots__ = ("data", "next")
def __init__(self, data):
self.data = data
self.next = None
This can cut memory usage per node substantially, since Python no longer needs to allocate a dictionary for instance attributes — it uses a fixed-size array-like structure instead. The trade-off is that I lose the ability to dynamically add new attributes to a node instance that weren’t declared in __slots__.
Node with Additional Metadata
Depending on the use case, I sometimes extend the node to carry more than just a single data value — for example, a priority, a timestamp, or in the case of a doubly linked list, a reference to the previous node as well:
class DoublyNode:
__slots__ = ("data", "next", "prev")
def __init__(self, data):
self.data = data
self.next = None
self.prev = None
I’ll cover the full doubly linked list implementation in more detail elsewhere, but it’s worth mentioning here because the node design decision — whether to include a prev reference — is made at this exact stage.
Real-World and Practical Use Cases
Nodes by themselves aren’t very useful, but as the building block for larger structures, they show up everywhere:
- Linked lists: the foundational use case, useful when frequent insertions/removals at arbitrary positions are needed without shifting elements like an array would require.
- Trees: a tree node is really just a node with multiple “next” references (children) instead of one.
- Graphs: adjacency list representations often use node-like structures to represent edges.
- Undo/redo systems: a doubly linked list of “state nodes” is a common way to implement undo/redo functionality in applications.
- LRU caches: a common professional implementation of a Least Recently Used cache uses a doubly linked list of nodes combined with a dictionary for O(1) access and reordering.
Common Mistakes When Writing Node Classes
- Forgetting to initialize
nexttoNone. If I skip this, I have to remember to set it manually every time I create a node, which is error-prone. - Confusing
self.nextwith the built-innext()function. I make sure never to shadow built-in names with attribute names in a way that causes confusion in the surrounding code. - Not handling circular references properly. If a linked list accidentally becomes circular (the last node points back to an earlier node instead of
None), infinite loops during traversal are a very real risk. - Comparing nodes by data instead of identity. If I want to check whether two variables point to the same node object, I use
is, not==, unless I’ve explicitly implemented__eq__.
Debugging Tips
When my linked list traversal misbehaves, the first thing I check is whether every node’s next pointer is set correctly. A simple debug helper I like to use:
def print_list(head):
nodes = []
current = head
seen = set()
while current is not None:
if id(current) in seen:
nodes.append("...(cycle detected)")
break
seen.add(id(current))
nodes.append(repr(current))
current = current.next
print(" -> ".join(nodes))
print_list(node1)
This little function protects me from infinite loops when debugging a broken list with an accidental cycle, since it tracks node identities it’s already visited.
Best Practices I Follow
- Always initialize
next(andprev, if applicable) explicitly in__init__. - Add a
__repr__method early — it pays for itself many times over during debugging. - Use
__slots__when working with large numbers of nodes to reduce memory overhead. - Add type hints for maintainability in bigger codebases.
- Keep the node class minimal — business logic belongs in the linked list class, not the node itself.
FAQs
Q: Is a Node its own data structure, or just part of one? A node is a building block, not a data structure by itself. A linked list is built from a chain of nodes.
Q: Can I store multiple values in one node? Yes — data can be any Python object, including a tuple, dictionary, or custom object holding multiple fields.
Q: Does Python have a built-in Node class? No, Python doesn’t provide one natively; linked list nodes are typically hand-written or come from third-party data structure libraries.
Q: Why not just use a Python list instead of writing my own linked list nodes? Python’s built-in list is implemented as a dynamic array, which is excellent for random access and appending at the end, but linked lists offer O(1) insertion/removal at arbitrary positions (once you have a reference to the node) without shifting elements.
Troubleshooting Tips
- If traversal never terminates, check for accidental cycles in the
nextreferences. - If you get
AttributeError: 'Node' object has no attribute 'x'after adding__slots__, remember that__slots__restricts which attributes instances can have — add any missing attribute names to the tuple. - If memory usage is higher than expected with many nodes, consider
__slots__or a more compact representation likearrayornumpyif the data is homogeneous.
Summary
Writing a simple linked list node in Python is one of those exercises that looks almost trivial but teaches me a lot about how Python objects work under the hood — memory layout, references versus values, and the trade-offs between flexibility and efficiency. Once I have a solid, well-designed Node class, building an entire linked list, stack, queue, or even a tree becomes a matter of composing these small building blocks together.