Tensor Flow 📂 Graphs and Speed with tf.function · 3 of 5 32 min read

Common tf.function Mistakes: Python Side Effects and Retracing

Avoid the five classic tf.function traps: Python side effects that run only once, frozen NumPy randomness, creating tf.Variables inside, calling .numpy() on symbolic tensors, and endless retracing. Each bug has real output and a fix. Hunt bugs line by line across three calls and count retraces for different loops.

Section 01

The Story — The Recipe Card With Today's Date

Written Once, Copied Forever
A chef writes a recipe card on Monday. At the top, he writes "Today's special price: ₹250", because that was Monday's price. Then the card is photocopied for every cook in the kitchen.

On Friday the price has changed. But every copy still says ₹250. The card was written once, and whatever was true at that moment is frozen into every copy.

A tf.function graph is that card. Anything Python does during tracing — printing, counting, reading the clock, rolling NumPy dice — happens once and is frozen. Only TensorFlow ops run again on every call.

This lesson covers the five mistakes that almost every TensorFlow learner makes with tf.function. Each one has the same root cause: mixing up trace time (Python runs) and run time (only the graph runs).

📄
Python side effects
print, append, +=
Happen once, at trace time.
🎲
Frozen values
np.random, time.time
Become constants in the graph.
📝
Variables inside
tf.Variable(...)
Raise a ValueError.
🚫
.numpy() inside
x.numpy()
Symbolic tensors have no values.
🔁
Retracing
f(i), new shapes
Slow, and a warning in the log.
💡
The fix pattern
use TF ops
tf.print, tf.Variable, tf.random, tensors.

Section 02

Mistake 1 — Python Side Effects

A side effect is anything a function does besides returning a value: printing, adding to a list, changing a global counter.

import tensorflow as tf

calls = 0
results = []

@tf.function
def step(x):
    global calls
    calls += 1                 # Python side effect
    results.append(x * 2)      # Python side effect
    return x * 2

for i in range(3):
    step(tf.constant(i))

print("calls counted :", calls)
print("results list  :", results)
OUTPUT
calls counted : 1 results list : [<tf.Tensor 'mul:0' shape=() dtype=int32>]

We called the function 3 times, but the counter says 1. The list holds one symbolic tensor, not three numbers. Python ran only while tracing.

The Fix: Keep State in TensorFlow

calls = tf.Variable(0)

@tf.function
def step(x):
    calls.assign_add(1)                           # a graph op: runs every call
    return x * 2

outputs = [int(step(tf.constant(i))) for i in range(3)]   # collect OUTSIDE
print("calls counted :", calls.numpy())
print("outputs       :", outputs)
OUTPUT
calls counted : 3 outputs : [0, 2, 4]

To collect values inside a graph loop, use tf.TensorArray:

@tf.function
def squares(n):
    ta = tf.TensorArray(tf.int32, size=n)
    for i in tf.range(n):
        ta = ta.write(i, i * i)
    return ta.stack()

print(squares(tf.constant(5)).numpy())
OUTPUT
[ 0 1 4 9 16]

Section 03

Mistake 2 — NumPy Randomness and the Clock Are Frozen

import numpy as np

@tf.function
def add_noise(x):
    return x + np.random.randn()      # NumPy runs once -> a constant

print("NumPy noise:", [round(float(add_noise(tf.constant(0.0))), 4) for _ in range(3)])

@tf.function
def add_noise_tf(x):
    return x + tf.random.normal([])   # a TF op -> new value each call

print("TF noise   :", [round(float(add_noise_tf(tf.constant(0.0))), 4) for _ in range(3)])
OUTPUT
NumPy noise: [0.7951, 0.7951, 0.7951] TF noise : [1.1394, 0.6833, -0.1051]
⚠️
A Silent Bug

There is no error and no warning. Your "random" data augmentation simply applies the same noise to every batch. Use tf.random.*, and tf.timestamp() instead of time.time().


Section 04

Mistake 3 — Creating Variables Inside the Function

@tf.function
def bad_layer(x):
    w = tf.Variable(2.0)       # a new variable on every trace
    return w * x

try:
    bad_layer(tf.constant(3.0))
except ValueError as e:
    print("ValueError:", str(e).strip().splitlines()[-1].split("ValueError: ")[-1][:95], "...")
OUTPUT
ValueError: tf.function only supports singleton tf.Variables created on the first call. Make sure the tf.Va ...

Create variables outside the function, or only once — the same pattern Keras layers use in build().

class Scale(tf.Module):
    def __init__(self):
        self.w = None

    @tf.function
    def __call__(self, x):
        if self.w is None:                 # create only on the first trace
            self.w = tf.Variable(2.0)
        return self.w * x

s = Scale()
print(s(tf.constant(3.0)).numpy(), s(tf.constant(4.0)).numpy())
OUTPUT
6.0 8.0

Section 05

Mistake 4 — Calling .numpy() Inside

@tf.function
def to_list(x):
    return x.numpy()           # no values exist during tracing

try:
    to_list(tf.constant([1, 2]))
except AttributeError as e:
    print("AttributeError:", str(e).strip().splitlines()[-1].split("AttributeError: ")[-1])
OUTPUT
AttributeError: 'SymbolicTensor' object has no attribute 'numpy'

Inside a tf.function, tensors are symbolic. Keep the maths in TF ops, and call .numpy() on the result, outside the function.


Section 06

Mistake 5 — Retracing Again and Again

@tf.function
def double(x):
    return x * 2

for i in range(5):
    double(i)                  # Python ints: each new value is a new signature
print("Python ints  -> traces:", double.experimental_get_tracing_count())

@tf.function
def double_t(x):
    return x * 2

for i in range(5):
    double_t(tf.constant(i))   # tensors: same shape and dtype every time
print("Tensors      -> traces:", double_t.experimental_get_tracing_count())
OUTPUT
Python ints -> traces: 5 Tensors -> traces: 1

After 5 retraces, TensorFlow prints a warning like this in your log:

LOG
WARNING:tensorflow:5 out of the last 5 calls to <function double> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors.
CauseExampleFix
Python numbers as argumentsf(i) in a loopPass tf.constant(i)
Changing tensor shapeslast batch is smallerinput_signature or reduce_retracing=True
tf.function created in a looptf.function(g)(x) inside forCreate it once, outside
Python objects that changea new list or dict each callPass tensors, or fixed objects

Section 07

Try It — Bug Hunter

🐛 Call It Three Times — What Really Runs? Interactive

Pick a buggy function. Press Call 3 times and watch which lines light up on each call. Amber = Python, runs only while tracing. Green = graph op, runs every call. Then press Show fix.

Code

Timeline

You expect

You get


Section 08

Try It — Retrace Counter

🔁 How Many Traces Will This Loop Cause? Interactive

Pick what the loop passes to f and how many times it loops. Each square is one call. Red = trace, green = cache hit.


Section 09

Golden Rules

🐛 tf.function Traps — Rules to Remember
1
Python code runs at trace time. Only TF ops run on every call. Ask: "is this a TF op?"
2
Use tf.print, tf.Variable.assign*, tf.random.* and tf.TensorArray for anything that must happen each call.
3
Create tf.Variables outside the function, or only once on the first call.
4
Never call .numpy() inside a tf.function. Call it on the result.
5
Pass tensors, not Python numbers. Watch experimental_get_tracing_count() and the retracing warning.
6
When stuck, turn on tf.config.run_functions_eagerly(True) to debug, then turn it off.