The single most important model to lock in your head
TF 2.x runs in two modes you switch between depending on goal: eager for debugging, graph for speed. The decorator that flips between them is @tf.function. Get this mental model right now and the entire rest of TF makes sense.
Eager mode: every op runs immediately, returns a concrete tensor, and you can call .numpy() on it. print(x) works. pdb works. x.shape reflects the actual data. This is the default in TF 2.x and the only mode that really feels like Python.
Graph mode: when you wrap a function in @tf.function, TF traces the first call — running it with symbolic placeholder tensors and recording a static computation graph. From the second call onward, that compiled graph runs directly on the runtime, skipping Python overhead and enabling op fusion plus XLA. Speedups of 2–10× are typical for tight inner loops.
print() inside a @tf.function-decorated function only fires during tracing, not on every call. If your prints suddenly stopped, that's why. Use tf.print() instead — it's part of the traced graph.