There are several different concrete node classes: PandaNode, GeomNode, CollisionNode, TextNode, and so on. (They don’t all end in the name “Node”; for instance, Camera and Character are also concrete node classes.)
Instances of these classes are used to construct the scene graph. Most of the meat of the scene graph is PandaNode, which is the most generic type. The leaves are generally GeomNode, which is where all the vertices are stored.
NodePath is just a general handle to a node class of any type. Any node can have a NodePath associated with it. Once you have a NodePath, you can do convenient operations like nodePath.setPos() or nodePath.reparentTo(). These operations can also be done without a NodePath, but it’s clumsier. So we normally keep a NodePath around to any object we’re going to manipulating a lot.
Actually, this isn’t true. At least it shouldn’t be. In general, if two NodePaths reference the same instance of a given node, then npA == npB. We go out of our way to ensure this in C++, and it should be true in Python as well.
It might not be the case if you are deriving a custom class from NodePath, though, for instance, Actor. In this case, you might have an Actor A and a NodePath B that reference the same node, but A != B because they are different class types. That’s just a Python thing, though.
Note that a node may have multiple different instances, e.g. multiple parents, or multiple grandparents, or whatever–this means that you might have two different NodePaths which both refer to the same node, but each is a different instance. This possibility is the whole reason we have the NodePath class in the first place: a NodePath encapsulates a unique path to the node from the root, hence its name.