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
TensorFlowNumPy equivalentNote
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.reshapenp.reshape-1 infers one dimension
tf.matmul / @@Batch dimensions broadcast
tf.reduce_meannp.meanTakes 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.function traces 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.function only 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 values

Shapes 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.

Building a Keras model Tensors and autograd

Last refreshed 2026-09-18.