==============================================================================================
RAHUL'S ML BLOG -- notes on machine learning, worked out by hand est. 2026
==============================================================================================
home | about | archive | glossary | contact
----------------------------------------------------------------------------------------------
CHAPTER 21 . SELF, NET, AND THE CALL THAT DOES NOT LOOP . PART 1 OF 2
One Dot Away From An Infinite Loop
============================================================================================
Every policy in this book writes its forward method the same way: build a
stack of layers once, in __init__, and store it under the name self.net.
Then, every time a prediction is needed, forward does one thing with it:
self.net(state). That line is a function call written on an object that is
not, itself, a function -- self.net is a pile of weight numbers. And the
object making that call, self, is built the exact same way self.net is. So
the question this post answers: when forward calls self.net(state), why
does that not turn into forward calling forward calling forward, forever?
-------
TWO KINDS OF PARENTHESES
Answering that starts somewhere with no machine learning in it at all: a
plain class.
class Adder:
def __init__(self, n):
self.n = n
def __call__(self, x):
return x + self.n
Two separate lines use parentheses for two separate jobs:
add5 = Adder(5) # BUILDS an object: allocate it, run __init__, hand it back
add5(10) # RUNS that object: 10 + 5 = 15
Adder(5) and add5(10) both use (), but the first makes a new Adder sitting
in memory with n=5 inside it, and the second uses that already-built object
to compute something. Chained on one line, Adder(5)(10) does both: first
paren builds, second paren runs, and the answer is still 15.
-------
WHAT () MEANS WHEN THE THING IS NOT A PLAIN FUNCTION
add5(10) looks like a function call, but add5 is not a function -- it is a
data object, an Adder instance holding the number 5. So what does () even
mean here? Python's rule: x(y) means "look up the __call__ slot on x's
type, and run that, passing x and y to it." Concretely, add5(10) means
type(add5).__call__(add5, 10), which is Adder.__call__(add5, 10), which
runs return x + self.n with x=10 and self=add5, giving 15.
There is no separate category of "things allowed to use parentheses."
There is only: does this object's type have a __call__ slot filled in? A
plain def function's type fills that slot with "run my bytecode." Adder
fills it with the method shown above. nn.Module -- the base class every
layer and every policy in this book inherits from -- fills that slot with
a short routine that does one thing: call self.forward(x) and hand back
the result. That single fact is why self.net(state) and policy(state) are
legal at all: self.net and policy are not functions, but their type
defines __call__, so parentheses work on them the same way they worked on
add5.
-------
SELF.NET IS A DIFFERENT OBJECT, NOT A DIFFERENT NAME FOR SELF
Here is the fact that breaks the worry. self.net is built exactly once,
inside the policy's __init__, and stored under that name:
def __init__(self, ...):
self.net = nn.Sequential(*layers) # built ONCE, right here
From that line on, self and self.net are two separate objects living at
two separate addresses. self is the policy -- it has its own forward
method, the one written by hand for this book (concatenate the inputs,
hand them to self.net, reshape the output). self.net is the layer stack --
it has its OWN forward, written by PyTorch, that is just a loop over
Linear and ReLU layers. Two objects, two different forward methods, two
different bodies of code.
-------
THE CALL TRACE, ONE DOT AT A TIME
So follow policy(state) all the way down, one hop per line:
policy(state)
= type(policy).__call__(policy, state) -- nn.Module's routine
-> policy.forward(state) -- the policy's own code
-> self.net(state) -- a DIFFERENT object now
= type(self.net).__call__(self.net, state)
-> self.net.forward(state) -- PyTorch's layer-loop code
-> layer 1 -> layer 2 -> layer 3 -> output tensor
<- returns the tensor
<- policy.forward uses that tensor, returns it
<- returns
Every arrow down is a hop to code written for a DIFFERENT object -- first
the policy's own forward, then, at self.net(state), a hop sideways to a
second object's forward. No arrow ever points back up into policy.forward.
The call ends because it runs out of different objects to hop to, not
because anyone stopped it.
-------
WHAT WOULD HAPPEN WITH THE MISSING DOT
The whole guarantee rests on that one dot. Compare the real line against
the one-character slip that removes it:
self.net(state) # hop to a DIFFERENT object -- policy.forward -> net.forward -> done
self(state) # hop to policy itself again -- policy.forward -> policy.forward -> ...
self(state) means type(policy).__call__(policy, state), which is exactly
the call that is already running. It would call policy.forward again,
which would call self(state) again, which would call policy.forward
again -- the same two lines, forever, until the machine's call stack
fills up and the program crashes. The .net is not decoration; it is the
entire reason the recursion terminates.
-------
WHY THE LAYERS ARE BUILT ONCE, NOT ON EVERY CALL
That answers why forward does not loop. A second question follows right
behind it: why does __init__ build self.net once, instead of building it
fresh on every call? One more line could look equivalent to self.net(state)
and is not: nn.Sequential(*layers)(state), built and run fresh on every
single call.
It would work -- it would return a number -- but every call would
construct a brand-new stack of layers with brand-new random weights,
run the input through them once, and throw them away. The training loop
edits the numbers inside self.net, in place, thousands of times. A
freshly-built stack has never been touched by any of those edits. Calling
self.net(state) reuses the one object that all that training landed on;
rebuilding it every call would reuse nothing, and the policy would never
get any smarter no matter how long it trained.
-------
ONE BREATH
self.net(state) is legal because self.net's type -- inherited from
nn.Module -- fills in a __call__ slot that runs self.forward, the same
mechanism that makes a plain callable object like Adder(5)(10) legal.
self.net is a different object from self, built once in __init__ and
never rebuilt, so calling it hops sideways to a different forward method
instead of back into the one already running. Drop the .net and the same
machinery calls the current object's own forward again, and again,
forever, until the stack overflows. One dot is the entire difference
between a network that runs and one that never returns.
SEAM. Pencil ends here; below, the same calls in Python.
-------
```python
# --------------------------------------------------------------------------
# A plain-Python stand-in for nn.Module's __call__ / forward split.
# No torch needed -- this is the object-model mechanics underneath it.
# --------------------------------------------------------------------------
class Module:
def __call__(self, x):
print(" __call__ runs on a", type(self).__name__, "object")
return self.forward(x)
class Layers(Module):
def __init__(self, weight):
self.weight = weight # one number standing in for a whole nn.Sequential
def forward(self, x):
result = x * self.weight
print(" Layers.forward runs: x * weight =", x, "*", self.weight, "=", result)
return result
class Policy(Module):
def __init__(self, weight):
self.net = Layers(weight) # built ONCE, stapled onto self.net
def forward(self, x):
print(" Policy.forward runs, hands x to self.net")
out = self.net(x) # hop to a DIFFERENT object -- not self(x)
print(" Policy.forward got", out, "back from self.net, returns it")
return out
policy = Policy(weight=3.0)
print("calling policy(5.0):")
result = policy(5.0)
print("final result:", result)
# --------------------------------------------------------------------------
# The missing-dot version: self(x) instead of self.net(x).
# Proven to never return, by actually letting it crash.
# --------------------------------------------------------------------------
import sys
class SilentModule: # same __call__->forward rule, no prints (the crash speaks for itself)
def __call__(self, x):
return self.forward(x)
class BrokenPolicy(SilentModule):
def __init__(self, weight):
self.weight = weight
def forward(self, x):
out = self(x) # self(x), NOT self.net(x) -- calls its OWN __call__ again
return out * self.weight
broken = BrokenPolicy(weight=3.0)
sys.setrecursionlimit(200)
try:
broken(5.0)
except RecursionError as e:
print("crashed:", type(e).__name__, "-- forward called itself until the stack ran out")
```
Running this code prints:
calling policy(5.0):
__call__ runs on a Policy object
Policy.forward runs, hands x to self.net
__call__ runs on a Layers object
Layers.forward runs: x * weight = 5.0 * 3.0 = 15.0
Policy.forward got 15.0 back from self.net, returns it
final result: 15.0
crashed: RecursionError -- forward called itself until the stack ran out
The first trace shows exactly two __call__ hops -- one onto the Policy
object, one onto the Layers object -- before anything returns. The second
block is the same two lines with one dot removed, and it does not print a
result at all: it hits Python's own recursion limit and crashes, because
self(x) keeps hopping back onto the same object that is already mid-call.
Real policies in this book never write that line; every one of them
reaches for self.net.
Part 2 opens self.net back up once training starts: what those weight
numbers actually are, why editing them in place across a training run is
the entire mechanism of learning, and why the same self.net gets called
with a batch of 128 during training and exactly 1 during evaluation.
-------
>> NOTE: STANDARD JARGON
__call__ = the slot a Python type fills in to make its instances usable with (); x(y) runs type(x).__call__(x, y)
dispatch = looking up which code actually runs for a given call, based on the object's type
nn.Module = PyTorch's base class for layers and policies; supplies a __call__ that runs self.forward
forward = the method that does the actual computation for one Module; never called directly, only through ()
RecursionError = Python's crash when a chain of calls never returns and the call stack fills up