Keras
UnboundLocalError
Python Error
Machine Learning
Debugging

Keras UnboundLocalError local variable 'logs' referenced before assignment

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When Keras raises UnboundLocalError for logs, the training loop usually reached callback code that expected a value that was never initialized. This often happens in custom callbacks, custom training loops, or version-mismatched examples copied from older APIs. The fix is to treat callback arguments defensively and ensure your code can run even when metrics are missing.

Why This Error Happens in Keras

The error message means Python saw a local variable named logs, but that variable was read before assignment in the same scope. In Keras callback methods like on_epoch_end, logs can be None or missing keys. If your code writes conditional assignments and then reads logs later, Python can fail before the training step is complete.

A common bad pattern is assigning logs inside one branch and using it outside the branch. Another issue is shadowing the callback argument with a local variable named logs. The callback method already provides the argument, so creating a second local variable with the same name can cause confusion and runtime errors.

python
1import tensorflow as tf
2
3class BadCallback(tf.keras.callbacks.Callback):
4    def on_epoch_end(self, epoch, logs=None):
5        if logs is not None and "loss" in logs:
6            logs = {"loss": float(logs["loss"])}
7        # If condition did not run as expected, this line can fail in similar patterns
8        print("loss:", logs["loss"])

The safe pattern is to normalize logs immediately and never assume keys exist.

Build a Safe Custom Callback

Initialize logs to an empty dictionary as soon as the callback starts. Then use .get for optional metrics. This keeps the callback stable even if metric names change, if validation is disabled, or if custom training code skips a metric in one epoch.

python
1import tensorflow as tf
2
3class SafeMetricsCallback(tf.keras.callbacks.Callback):
4    def on_epoch_end(self, epoch, logs=None):
5        logs = logs or {}
6        loss = logs.get("loss")
7        val_loss = logs.get("val_loss")
8
9        if loss is None:
10            print(f"epoch {epoch}: loss not reported")
11            return
12
13        if val_loss is None:
14            print(f"epoch {epoch}: loss={loss:.4f}")
15        else:
16            print(f"epoch {epoch}: loss={loss:.4f}, val_loss={val_loss:.4f}")
17
18
19def build_model():
20    model = tf.keras.Sequential([
21        tf.keras.layers.Input(shape=(4,)),
22        tf.keras.layers.Dense(8, activation="relu"),
23        tf.keras.layers.Dense(3, activation="softmax"),
24    ])
25    model.compile(
26        optimizer="adam",
27        loss="sparse_categorical_crossentropy",
28        metrics=["accuracy"],
29    )
30    return model
31
32x = tf.random.normal((200, 4))
33y = tf.random.uniform((200,), minval=0, maxval=3, dtype=tf.int32)
34
35model = build_model()
36model.fit(x, y, epochs=3, batch_size=16, callbacks=[SafeMetricsCallback()])

This script runs end to end and avoids the UnboundLocalError pattern because every path defines the variables before use.

Debugging Version and API Mismatch

Keras examples on older blogs can use callback behavior from earlier TensorFlow releases. Before copying code, print versions and verify metric names that appear in logs. It is common to expect acc while modern builds emit accuracy.

python
1import tensorflow as tf
2
3print("TensorFlow version:", tf.__version__)
4
5class InspectCallback(tf.keras.callbacks.Callback):
6    def on_epoch_end(self, epoch, logs=None):
7        logs = logs or {}
8        print("available keys:", sorted(logs.keys()))
9
10x = tf.random.normal((32, 4))
11y = tf.random.uniform((32,), minval=0, maxval=3, dtype=tf.int32)
12
13m = tf.keras.Sequential([
14    tf.keras.layers.Input(shape=(4,)),
15    tf.keras.layers.Dense(3, activation="softmax"),
16])
17m.compile(optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"])
18m.fit(x, y, epochs=1, callbacks=[InspectCallback()])

Using an inspection callback once during development helps you lock in the exact metric keys and avoid fragile callback code.

Common Pitfalls

  • Reusing the name logs for a new local variable and accidentally hiding the callback argument.
  • Assuming logs is always populated even when a run is interrupted or configured with minimal metrics.
  • Reading logs["val_loss"] when no validation data is passed to fit.
  • Copying examples that rely on deprecated metric names such as acc without checking current output.
  • Catching broad exceptions around callbacks, which hides the real control-flow error and makes debugging harder.

Summary

  • UnboundLocalError for logs is a Python scoping issue, not a Keras math issue.
  • Normalize callback input with logs = logs or {} at the start of each method.
  • Use .get and key checks instead of direct indexing for optional metrics.
  • Verify TensorFlow version and inspect emitted metric keys before finalizing callback code.
  • Keep callback logic simple and explicit so every execution path initializes needed variables.

Course illustration
Course illustration

All Rights Reserved.