Why TF feels like NumPy now
TF 2.x runs eagerly by default — every op executes immediately, returns a concrete tensor, and you can introspect it with .numpy(). This is what makes it feel like Python instead of a graph compiler.
NumPy interop is seamless in both directions. TF ops accept NumPy arrays implicitly, and TF tensors implement the NumPy __array__ protocol so they slot into NumPy functions without conversion. tf.convert_to_tensor and .numpy() are the explicit conversion functions when you want them.
The .numpy() exception: The
.numpy() method works only in eager mode. Inside a @tf.function-decorated function, tensors are symbolic — they have no concrete value at trace time, and .numpy() raises. Use tf.print() instead.