The Story — A Taxi and a Train
A train runs on a fixed route that was planned in advance. Nobody gives directions on the way. It just goes, fast. You cannot change the route mid-journey, but for a trip you make every day, it is much faster.
Eager mode is the taxi: Python tells TensorFlow each op, one at a time. Graph mode is the train: TensorFlow plans the whole route once, then runs it again and again without asking Python.
In this module you will learn how TensorFlow turns Python code into a graph, why that makes it faster, what can go wrong, and how to measure the speed-up yourself. This first lesson explains the two modes side by side.
Eager Mode — The Default
In TensorFlow 2, every op runs the moment Python reaches it. You get real numbers back at once.
import tensorflow as tf
def layer(x, w, b):
return tf.nn.relu(x * w + b)
x, w, b = tf.constant(3.0), tf.constant(-2.0), tf.constant(4.0)
y = layer(x, w, b)
print("eager result :", y)
print("running eager:", tf.executing_eagerly())
Behind the scenes, Python walks line by line. For each op it calls into TensorFlow's C++ core, waits for the answer, then moves to the next op.
Graph Mode — Plan Once, Run Many Times
Wrap the same function with tf.function. The first call builds a graph: a data structure where nodes are ops and edges are the tensors flowing between them. Later calls run that graph directly in C++.
graph_layer = tf.function(layer)
print("graph result :", graph_layer(x, w, b))
concrete = graph_layer.get_concrete_function(x, w, b)
ops = [op.type for op in concrete.graph.get_operations()]
print("ops in graph :", ops)
Placeholder nodes are the inputs (x, w, b). They have a shape and dtype, but no value until the graph runs. Mul, AddV2 and Relu are your maths. Identity marks the output. This graph is the "train route". It can run with no Python at all.
The progress bars show the same 1,000 small ops. The graph finishes first because it skips most of the Python chatter.
Try It — Build and Run a Graph
Type an expression using the inputs x, w, b, numbers, + - * / and relu sigmoid exp square. Press Build graph to trace it. Then press Run to push values through. Try x * (2 + 3) + b and press Optimise to see constant folding.
Why Graphs Are Useful
Eager vs Graph — Side by Side
| Question | Eager mode | Graph mode (tf.function) |
|---|---|---|
| When does an op run? | Immediately | When the whole graph is called |
| Can I print values? | Yes, with print() | Use tf.print() |
| Can I use .numpy()? | Yes | No — tensors are symbolic inside |
| Python overhead | Every op | Once per call |
| Graph optimisations | No | Yes (Grappler, optional XLA) |
| Export without Python | No | Yes |
| Best for | Learning, debugging, one-off maths | Training loops, inference, deployment |
A Handy Switch for Debugging
If a tf.function misbehaves, you can force every tf.function to run eagerly, find the bug, then switch back.
@tf.function
def where_am_i():
return tf.executing_eagerly()
print("normal : eager inside?", bool(where_am_i()))
tf.config.run_functions_eagerly(True) # debug: all tf.functions run eagerly
print("debug mode : eager inside?", bool(where_am_i()))
tf.config.run_functions_eagerly(False) # back to fast graph mode
Golden Rules
tf.function once the code works.tf.config.run_functions_eagerly(True) to debug a graph, then turn it off.