TensorFlow
map_fn
conditional logic
TensorFlow programming
neural networks

trying to use if in tensorflow's map_fn

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

Introduction

Using conditional logic inside tf.map_fn is a common source of confusion because TensorFlow graph tracing is not the same as plain Python execution. A Python if decides immediately, while graph code needs branch operations represented as TensorFlow ops. The fix is to express branch behavior with tf.cond or to avoid map_fn entirely when a vectorized op can do the same work.

Why Python if Fails in Graph-Oriented Paths

In traced functions, Tensor values are symbolic. Python cannot evaluate a symbolic predicate at trace time.

python
1import tensorflow as tf
2
3x = tf.constant([1, -2, 3], dtype=tf.int32)
4
5@tf.function
6def bad_map(values):
7    def fn(v):
8        if v > 0:  # invalid branch test for symbolic tensor
9            return v * 2
10        return v
11    return tf.map_fn(fn, values)
12
13try:
14    print(bad_map(x))
15except Exception as e:
16    print(type(e).__name__, e)

The exact error text can vary by TensorFlow version, but the root cause is the same: branch logic is not expressed as TensorFlow operations.

Correct Pattern With tf.cond

For per-element branching in map_fn, use tf.cond.

python
1import tensorflow as tf
2
3x = tf.constant([1, -2, 3, -4], dtype=tf.int32)
4
5@tf.function
6def cond_map(values):
7    def fn(v):
8        return tf.cond(v > 0, lambda: v * 2, lambda: v)
9    return tf.map_fn(fn, values, fn_output_signature=tf.int32)
10
11print(cond_map(x).numpy())

Important details:

  • Both branches must return the same dtype and compatible shape.
  • Use fn_output_signature to make tracing stable and explicit.

Prefer Vectorization for Elementwise Logic

If the condition is elementwise, tf.where is usually faster and simpler than map_fn.

python
1import tensorflow as tf
2
3x = tf.constant([1, -2, 3, -4], dtype=tf.int32)
4y = tf.where(x > 0, x * 2, x)
5print(y.numpy())

map_fn has overhead because it applies a function repeatedly. Vectorized expressions let TensorFlow optimize the whole operation at once.

Complex Branching With Structured Outputs

Sometimes each element produces more than one value. In that case, return a fixed structured tensor from both branches.

python
1import tensorflow as tf
2
3rows = tf.constant([[2, 3], [-1, 5], [0, 4]], dtype=tf.int32)
4
5@tf.function
6def classify(values):
7    def fn(row):
8        s = tf.reduce_sum(row)
9        return tf.cond(
10            s > 0,
11            lambda: tf.stack([s, 1]),
12            lambda: tf.stack([s, 0])
13        )
14
15    return tf.map_fn(fn, values, fn_output_signature=tf.TensorSpec(shape=(2,), dtype=tf.int32))
16
17print(classify(rows).numpy())

By enforcing fixed shape and dtype, you avoid retracing issues and runtime shape errors.

Debugging and Testing Strategy

Debug graph functions with small deterministic tensors first. Then test edge cases:

  • All values pass true branch.
  • All values pass false branch.
  • Mixed signs and zero.
  • Empty inputs where supported.

A lightweight test function helps keep behavior stable across upgrades.

python
1def run_checks():
2    vals = tf.constant([0, 5, -3], dtype=tf.int32)
3    out = cond_map(vals).numpy().tolist()
4    assert out == [0, 10, -3], f"unexpected output {out}"
5
6run_checks()
7print("checks passed")

This style catches regressions early when model preprocessing code changes.

Performance Notes

Use tf.map_fn when each element needs non-trivial logic that is awkward to vectorize. For simple arithmetic or thresholding, vectorized code will usually outperform map-based transforms and be easier to maintain. Also avoid unnecessary Python side effects inside mapped functions because they may not behave predictably in graph mode.

Common Pitfalls

A common pitfall is mixing eager and graph assumptions in the same function. Code that appears valid in eager mode can fail under tf.function. Another issue is mismatched branch outputs in tf.cond, where one branch returns scalar and the other returns vector. Teams also omit fn_output_signature, which can trigger ambiguous tracing behavior with nested structures. Overuse of map_fn is another pattern. Many preprocessing pipelines become slower than necessary because vectorized alternatives were not considered.

Summary

  • Python if is not reliable for symbolic tensors inside tf.map_fn.
  • Use tf.cond for branch logic when mapping per element.
  • Prefer tf.where or other vectorized ops for simple elementwise conditions.
  • Keep branch return shapes and dtypes aligned and explicit.
  • Add deterministic tests to protect preprocessing behavior during refactors.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

All Rights Reserved.