Tensor Flow 📂 Graphs and Speed with tf.function · 4 of 5 29 min read

input_signature and tf.TensorSpec: Control Tracing in TensorFlow

Use tf.TensorSpec to describe the shape and dtype a tensor must have, with None for any size. Add input_signature to trace a tf.function only once, reject bad inputs early, handle the smaller last batch, and export a SavedModel with a clear API. Test any tensor against any spec in the live Signature Matcher.

Section 01

The Story — The Airport Bag Sizer

If It Fits the Frame, It Flies
At the airport gate there is a metal frame. Your cabin bag must fit inside it. The airline does not care what is in the bag, or whether it is a little shorter or fatter — only that it fits the frame.

Imagine instead that every new bag size needed a brand-new frame to be built. The queue would never move.

tf.TensorSpec is the frame. It describes the shape and dtype a tensor must have. input_signature tells a tf.function: "build one graph for everything that fits this frame, and reject anything that does not."

In the last lessons you saw that every new shape can cause a new trace. This lesson shows how to control tracing with an explicit signature. It also shows how the same signature becomes the public interface of a saved model.


Section 02

tf.TensorSpec — A Description of a Tensor

A TensorSpec holds a shape, a dtype and an optional name. It has no values. None in the shape means "any size on this axis".

import tensorflow as tf

spec = tf.TensorSpec(shape=[None, 3], dtype=tf.float32, name="features")
print(spec)

for t in [tf.zeros([5, 3]), tf.zeros([1, 3]), tf.zeros([5, 4]),
          tf.zeros([5, 3], tf.int32), tf.zeros([3])]:
    print(f"{str(t.shape):8s} {t.dtype.name:8s} fits? {spec.is_compatible_with(t)}")
OUTPUT
TensorSpec(shape=(None, 3), dtype=tf.float32, name='features') (5, 3) float32 fits? True (1, 3) float32 fits? True (5, 4) float32 fits? False (5, 3) int32 fits? False (3,) float32 fits? False
Spec shapeMeansAcceptsRejects
[32, 3]exactly 32 × 3(32, 3)(16, 3)
[None, 3]any rows, 3 columns(1, 3), (500, 3)(5, 4), (3,)
[None, None]any 2-D(2, 7), (9, 1)(3,), (2, 2, 2)
[]a scalar()(1,)
Noneany shape, any rankeverything with the right dtypewrong dtype
print(tf.TensorSpec.from_tensor(tf.zeros([2, 2])))     # copy a tensor's spec
print(tf.TensorSpec(shape=None, dtype=tf.float32))     # unknown rank
OUTPUT
TensorSpec(shape=(2, 2), dtype=tf.float32, name=None) TensorSpec(shape=<unknown>, dtype=tf.float32, name=None)

Section 03

input_signature — One Graph for Every Batch Size

@tf.function(input_signature=[tf.TensorSpec(shape=[None, 3], dtype=tf.float32)])
def row_sums(x):
    print("  tracing with", x.shape)
    return tf.reduce_sum(x, axis=1)

for batch in [1, 8, 64, 5]:
    row_sums(tf.ones([batch, 3]))
print("traces:", row_sums.experimental_get_tracing_count())
OUTPUT
tracing with (None, 3) traces: 1

Four different batch sizes, one trace. The graph was built for shape (None, 3) and works for all of them.

Anything That Does Not Fit Is Rejected

for bad in [tf.ones([2, 4]), tf.ones([2, 3], tf.int32), 3.0]:
    try:
        row_sums(bad)
    except TypeError as e:
        print("TypeError:", str(e).split("`")[1])
OUTPUT
TypeError: Can not cast TensorSpec(shape=(2, 4), dtype=tf.float32, name=None) to TensorSpec(shape=(None, 3), dtype=tf.float32, name=None) TypeError: Can not cast TensorSpec(shape=(2, 3), dtype=tf.int32, name=None) to TensorSpec(shape=(None, 3), dtype=tf.float32, name=None) TypeError: Can not cast TensorSpec(shape=(), dtype=tf.float32, name=None) to TensorSpec(shape=(None, 3), dtype=tf.float32, name=None)
🛡️
A Signature Is Also a Safety Check

Without a signature, a wrong shape would quietly trace a new graph and fail somewhere deep inside with a confusing error. With a signature, the bad call fails at the door, with a message that names the expected spec.

Animated Diagram — The Spec Is a Gate
TensorSpec (None, 3) float32 one graphtraced once (8, 3) (64, 3) (5, 4) rejected: TypeError

Blue tensors fit the spec and share one graph. The red tensor has 4 columns, so the call fails before any tracing.


Section 04

Try It — Signature Matcher

📏 Does My Tensor Fit the Spec? Interactive

Write a spec shape with numbers and None (for example None, 28, 28), leave it empty for a scalar, or type any for unknown rank. Then list test tensors, one per line, as shape dtype.

Test tensorAxis by axisdtypeResult

Section 05

Several Inputs and Nested Inputs

Give one spec per argument, in order. Dicts and tuples work too — the signature mirrors the structure of the arguments.

@tf.function(input_signature=[
    tf.TensorSpec([None, 3], tf.float32, name="x"),
    tf.TensorSpec([None], tf.int32, name="labels"),
])
def batch_info(x, labels):
    return tf.shape(x)[0], tf.reduce_max(labels)

print([int(t) for t in batch_info(tf.ones([4, 3]), tf.constant([2, 0, 1, 2]))])

@tf.function(input_signature=[{
    "img":   tf.TensorSpec([None, 28, 28], tf.float32),
    "label": tf.TensorSpec([None], tf.int64),
}])
def count_images(batch):
    return tf.shape(batch["img"])[0]

print(count_images({"img": tf.zeros([6, 28, 28]), "label": tf.zeros([6], tf.int64)}).numpy())
OUTPUT
[4, 2] 6

Section 06

Where Signatures Really Matter

1. The Last Batch Is Smaller

100 samples in batches of 32 gives shapes 32, 32, 32 and then 4. Without a signature, that last batch triggers a second trace.

ds = tf.data.Dataset.range(100).map(lambda i: tf.fill([3], tf.cast(i, tf.float32))).batch(32)

@tf.function
def total_plain(x):
    return tf.reduce_sum(x)

@tf.function(input_signature=[tf.TensorSpec([None, 3], tf.float32)])
def total_sig(x):
    return tf.reduce_sum(x)

for x in ds:
    total_plain(x); total_sig(x)

print("batch shapes :", [tuple(x.shape) for x in ds])
print("traces plain :", total_plain.experimental_get_tracing_count())
print("traces sig   :", total_sig.experimental_get_tracing_count())
OUTPUT
batch shapes : [(32, 3), (32, 3), (32, 3), (4, 3)] traces plain : 2 traces sig : 1

2. Saving a Model for Serving

When you save a model, the signature becomes its API. Servers, phones and other languages call the model through it, with no Python code.

class Linear(tf.Module):
    def __init__(self):
        self.w = tf.Variable([[1.0], [2.0], [3.0]])

    @tf.function(input_signature=[tf.TensorSpec([None, 3], tf.float32, name="x")])
    def predict(self, x):
        return x @ self.w

tf.saved_model.save(Linear(), "/tmp/linear_model")
loaded = tf.saved_model.load("/tmp/linear_model")

print(loaded.predict(tf.ones([2, 3])).numpy().tolist())
print("signatures:", list(loaded.signatures.keys()))
OUTPUT
[[6.0], [6.0]] signatures: ['serving_default']
ToolHow it worksUse when
input_signature=[...]You fix the specs up front. Exactly one graph. Bad inputs are rejected.You know the shapes; exporting a SavedModel; strict APIs
reduce_retracing=TrueTensorFlow traces, then relaxes shapes to None after it sees them change.Shapes vary and you do not want to write specs
NeitherOne graph per exact shape and dtype.Shapes never change
⚠️
Only Tensors Allowed

With an input_signature, every argument must be a tensor that fits a spec. You cannot pass Python flags like training=True. Make the flag a tensor (tf.TensorSpec([], tf.bool)) or leave it out of the signed function.


Section 07

Golden Rules

📏 input_signature and TensorSpec — Rules to Remember
1
A TensorSpec is a shape + dtype description with no values. None means "any size here".
2
Use None for the batch axis: TensorSpec([None, features], tf.float32).
3
With input_signature a function traces once and rejects anything that does not fit.
4
All arguments must be tensors. Python flags must become tensors or move out.
5
Always add an input_signature to functions you export in a SavedModel.