Exercise 3: An isinstance Chain With No Final Raise — Possible Solution ==================================================================== THE DELIBERATELY INCOMPLETE FUNCTION ------------------------------ def contains_division(node): if isinstance(node, Literal): return False elif isinstance(node, Grouping): return contains_division(node.expression) elif isinstance(node, Unary): return contains_division(node.right) elif isinstance(node, Binary): if node.operator == '/': return True return contains_division(node.left) or contains_division(node.right) # no final else, no raise -- falls through here for any # unhandled node type, implicitly returning None RESULT ON A TREE CONTAINING A Variable NODE ------------------------------ var_tree = Binary(Variable('x'), '+', Literal(5.0)) contains_division(var_tree) -> False WHY THIS IS WRONG, NOT JUST INCOMPLETE ------------------------------ The Binary case recurses as: contains_division(node.left) or contains_division(node.right) node.left is the unhandled Variable('x') node. Because none of the isinstance checks match it, Python falls off the end of the function with no return statement at all -- which means it implicitly returns None. Python's `or` operator treats None as falsy, so: None or contains_division(node.right) -> None or False -> False The function returns a clean, ordinary-looking False -- not an error, not a crash, nothing that would draw attention to itself. If Variable had been something the function genuinely needed to check (e.g. a future node type that itself could contain a division, such as a function-call argument list), this would silently report "no division found" even when one was actually present deeper in the tree, purely because that node type was never taught to this specific function. WHAT THE VISITOR VERSION DOES INSTEAD ------------------------------ class DivisionChecker: def visit_literal(self, node): return False def visit_grouping(self, node): return node.expression.accept(self) def visit_unary(self, node): return node.right.accept(self) def visit_binary(self, node): if node.operator == '/': return True return node.left.accept(self) or node.right.accept(self) # deliberately no visit_variable defined var_tree2.accept(DivisionChecker()) -> AttributeError: 'DivisionChecker' object has no attribute 'visit_variable' WHY THIS WORKS AS AN ANSWER ------------------------------ This is the sharpest version of the chapter's own "failing loud vs. failing silent" finding. An isinstance chain's safety depends entirely on someone remembering to write an explicit final raise -- forget it, and an unhandled type doesn't error, it just quietly becomes None and gets absorbed by whatever boolean or arithmetic logic happens to sit around it, exactly as this exercise reproduces. The Visitor pattern's AttributeError requires no equivalent discipline: node.accept(visitor) calling visitor.visit_variable(self) on an object that never defined that method is not a design decision anyone made on purpose -- it's simply what Python does when you look up a method that isn't there. The safety isn't opt-in; it's a structural consequence of how double dispatch works.