Tensors and eager execution
Immutable constants, trainable variables, shape and dtype rules, and how tf.function turns Python into a traced graph.
Constants, variables and dtype
import tensorflow as tf
tf.__version__
a = tf.constant([[1.0, 2.0], [3.0, 4.0]]) # immutable
v = tf.Variable([1.0, 2.0, 3.0]) # mutable, watched for gradients
a.shape # TensorShape([2, 2])
a.dtype # <dtype: 'float32'> — constants default to float32
a.device # where the values live
v.assign([4.0, 5.0, 6.0]) # in-place update
v.assign_add([1.0, 1.0, 1.0])
v.numpy() # bridge to NumPy (eager mode)
b = tf.constant([[1, 2], [3, 4]]) # int32
a + b # InvalidArgumentError: dtypes must match
a + tf.cast(b, tf.float32) # OK| TensorFlow | NumPy equivalent | Note |
|---|---|---|
tf.constant(x) | np.array(x) | Immutable, may live on a GPU |
tf.Variable(x) | — | Trainable state; the thing optimisers update |
x.numpy() | — | Fails on a symbolic (graph) tensor |
tf.reshape | np.reshape | -1 infers one dimension |
tf.matmul / @ | @ | Batch dimensions broadcast |
tf.reduce_mean | np.mean | Takes an axis argument |
⚠️
TensorFlow defaults to
float32, NumPy to float64. Mixing them raises InvalidArgumentError, and Python floats in a computation can silently upcast a tensor and slow training. Cast deliberately with tf.cast at the boundary where data enters the model.Eager execution and graphs
# eager: runs immediately, easy to debug, like NumPy
x = tf.constant([1.0, 2.0, 3.0])
print(x * 2)
@tf.function
def step(x):
return tf.reduce_mean(tf.square(x))
step(tf.constant([1.0, 2.0, 3.0])) # traced once, then runs as a graph
step(tf.constant([4.0, 5.0, 6.0])) # reuses the same trace
# inside a tf.function you can still choose to break out for debugging
@tf.function
def debug_step(x):
tf.print("x is", x) # prints at graph run time
return tf.reduce_mean(x)- Eager is the default: every operation returns a value you can inspect with
.numpy(), which makes debugging straightforward. @tf.functiontraces the Python function into a graph, enabling optimisations and removal of Python overhead — a significant speedup inside training loops.- A trace is cached per input signature: a tensor with the same shape and dtype reuses it.
- Python side effects in a
tf.functiononly run during tracing, not on every call.
Reshaping and indexing
t = tf.reshape(tf.range(24), (2, 3, 4)) # (2, 3, 4)
t[0] # (3, 4)
t[:, 1, :] # (2, 4)
t[..., 0] # (2, 3) — ellipsis covers any leading axes
tf.transpose(t, perm=[0, 2, 1]).shape # (2, 4, 3)
tf.expand_dims(t, axis=-1).shape # (2, 3, 4, 1)
tf.squeeze(tf.zeros((1, 4, 1))).shape # (4,)
tf.concat([tf.zeros((2, 3)), tf.ones((2, 3))], axis=0).shape # (4, 3)
tf.stack([tf.zeros((4,)), tf.ones((4,))], axis=1).shape # (4, 2)# a variable used in a tf.function with a changing Python argument retraces
@tf.function
def scale(x, factor):
return x * factor
scale(tf.ones((2, 2)), 2.0) # trace 1 (int vs float matters)
scale(tf.ones((2, 2)), 3) # trace 2: different Python type
scale(tf.ones((2, 2)), tf.constant(2.0)) # trace 3: tensor, one trace for all valuesShapes in TensorFlow can contain None for a dimension that is not fixed yet, most often the batch size. A model built with batch_input_shape=(None, 28, 28, 1) accepts any batch size, which is what you want for both training and serving.
FAQ
Should I use tf.function everywhere?
Not inside the model-building code — Keras already wraps the training step. Apply it to your own hot loops and data-processing functions, and keep it off debugging code where you want to inspect intermediate values.
How do I move a tensor between CPU and GPU?
Place it in a scope:
with tf.device("/GPU:0"):. TensorFlow also copies automatically when an operation runs on a different device, which is convenient but shows up as a transfer cost in profiles.Related
Building a Keras model Tensors and autograd
Last refreshed 2026-09-18.