Exercise 2: Why init's this-Substitution Needs Two Separate Checks — Possible Solution ==================================================================== THE FULL CODE BEING TRACED ------------------------------ def call(self, interpreter, arguments): env = Environment(self.closure) for param, arg in zip(self.declaration.params, arguments): env.define(param, arg) try: interpreter.execute_block(self.declaration.body, env) except ReturnException as r: if self.is_initializer: return self.closure.get("this") return r.value if self.is_initializer: return self.closure.get("this") return None TRACING PATH 1 -- init WITH an explicit return ------------------------------ class Weird { init(x) { this.x = x; return "ignored"; } } `return "ignored";` inside the body raises ReturnException("ignored"). That's caught by the `except ReturnException as r:` block. Inside it, `self.is_initializer` is True (set when visit_class_stmt built this method with `is_init = method_decl.name == 'init'`), so the branch returns `self.closure.get('this')` -- the bound instance -- completely discarding `r.value` ("ignored"). Verified: `Weird(42).x` reads 42, confirming the instance, not the string, is what Weird(42) itself evaluates to. TRACING PATH 2 -- init WITHOUT any return statement ------------------------------ class Plain { init(x) { this.x = x; } } The body runs to completion with no `return` anywhere -- no exception is ever raised, so the `except ReturnException` block never executes at all. Execution falls through to the code AFTER the try/except: `if self.is_initializer: return self.closure.get("this")`. This is a SEPARATE `if self.is_initializer` check, textually identical to the one inside the except block, but reached via a completely different control-flow path -- the try body finished normally rather than being interrupted by an exception. WHY THIS WORKS AS AN ANSWER ------------------------------ The two checks can't be merged into "one shared check" because they sit on two different sides of a try/except boundary that Python itself enforces -- code in the `except ReturnException:` block only runs when an exception was actually caught, and code after the whole try/except statement only runs (via this path) when the try body completed WITHOUT raising anything. There's no single point in the function where both "a return happened" and "no return happened" cases pass through the same line, because those are two mutually exclusive event timelines by definition. Every init -- with or without its own explicit return statement -- needs the guarantee that calling the class yields the instance, so both of the two genuinely different paths a call can take (interrupted by return, or falling off the end) each need their own copy of the same one-line rule. This mirrors Chapter 7's own plain function call, which already had this same two-path shape (`except ReturnException` vs. falling through to `return None`) -- Chapter 8 just adds the same `is_initializer` override to both existing paths rather than introducing a new one.