Almost everyone writes the same first attempt at validating a binary search tree, and almost everyone's first attempt passes every test they can think up by hand. That is what makes it worth writing about: the bug is not in the code you can see, it is in the definition you thought you were implementing.
The attempt
Read it out loud and it sounds exactly like the definition: left is smaller, right is bigger, check every node. Draw a three-node tree and it is correct. Draw a seven-node tree and it is still correct. It will pass any example small enough to fit on a whiteboard.
The counterexample
Consider a root of 10, a left child of 5, and give that 5 a right child of 12. Every parent-child pair is fine: 5 is less than 10, and 12 is greater than 5. But 12 sits in the left subtree of 10, and everything in that subtree must be under 10. The tree is invalid and the code says it is fine.
The BST property constrains subtrees, not children. A node bounds an entire region of the tree, and a check that only looks one level down can never see that.
Fix one: carry the range down
Instead of comparing to the parent, pass the legal interval down. The root may be anything. A left child inherits its parent's lower bound and gains the parent's value as a new upper bound. The right child mirrors it.
Now the 12 arrives carrying the interval (5, 10) and fails immediately, three levels from where the mistake was made. That is the whole point: the constraint travelled.
Fix two: walk it inorder
An inorder traversal of a valid BST is sorted. So walk the tree inorder and check that each value beats the previous one. No bounds, no extra parameters.
They are the same idea
The inorder version is the bounds version unrolled. When you visit a node inorder, every ancestor constraint has already been applied by the order of the walk — the previous value is exactly the tightest lower bound in force. One version carries the constraint explicitly; the other lets the traversal order carry it.
- →Prefer bounds if you want an early exit and O(h) space.
- →Prefer inorder if you are already walking the tree for another reason.
- →If you keep the previous value in a plain variable, seed it with negative infinity, not None — otherwise the first comparison silently short-circuits.
The transferable lesson
Any time your recursion only inspects a node and its immediate children, stop and ask what an ancestor three levels up could still ruin. This shape recurs constantly: interval merging, range trees, segment trees, and validity checks on any structure where a node governs a region rather than a neighbour.
Reading about a pattern is not the same as producing it under time pressure. The problems that drill this are in the curriculum, in order.