Keras
subclassing API
call method
deep learning
machine learning

Why in Keras subclassing API, the call method is never called and as an alternative the input is passed by calling the object of this class?

Master System Design with Codemia

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

Introduction

In Keras subclassing, you usually execute a model with model(inputs) rather than calling model.call(inputs) directly. That can make it look as if call is somehow being skipped, but it is not. Keras routes execution through __call__, which then invokes your call method while also adding framework behavior such as shape handling, mask propagation, and graph tracing.

What Actually Happens When You Write model(x)

The user-facing syntax is:

python
outputs = model(x)

Internally, Keras runs the model’s __call__ method. That wrapper is responsible for more than just dispatching to your code. It also handles things such as:

  • Building layers lazily on first use.
  • Managing training and mask arguments.
  • Keras history tracking for symbolic graphs.
  • Dtype policy and casting.
  • Integration with tracing and SavedModel export.

Your custom call method is still executed, but it is executed through that managed path.

Minimal Example

Here is the standard subclassing pattern.

python
1import tensorflow as tf
2
3class MyModel(tf.keras.Model):
4    def __init__(self):
5        super().__init__()
6        self.d1 = tf.keras.layers.Dense(8, activation="relu")
7        self.d2 = tf.keras.layers.Dense(1)
8
9    def call(self, inputs, training=False):
10        x = self.d1(inputs)
11        return self.d2(x)
12
13model = MyModel()
14x = tf.random.normal((4, 3))
15y = model(x, training=True)
16
17print(y.shape)

Even though user code calls model(x), your call implementation is what computes the output.

Why Keras Discourages Direct call(...)

You can technically write:

python
y = model.call(x)

but that bypasses part of the framework contract. In simple eager experiments it may appear to work, yet subtle issues show up later with:

  • Mixed precision.
  • Nested models.
  • Masked inputs.
  • Distribution strategies.
  • Symbolic graph construction.

Keras wants model(x) to be the public entry point because that keeps all of its lifecycle and bookkeeping consistent.

Proving That call Is Executed

If the behavior still feels abstract, add a print inside call.

python
1import tensorflow as tf
2
3class DebugModel(tf.keras.Model):
4    def call(self, inputs, training=False):
5        tf.print("inside call")
6        return inputs * 2.0
7
8m = DebugModel()
9print(m(tf.constant([1.0, 2.0, 3.0])))

You will see inside call in the output. The method is not missing; it is simply reached through __call__.

The Role of training and Other Special Arguments

The wrapper path matters even more when you use layers such as dropout or batch normalization.

python
1import tensorflow as tf
2
3class DropModel(tf.keras.Model):
4    def __init__(self):
5        super().__init__()
6        self.drop = tf.keras.layers.Dropout(0.5)
7
8    def call(self, inputs, training=False):
9        return self.drop(inputs, training=training)
10
11m = DropModel()
12x = tf.ones((2, 4))
13
14print(m(x, training=True))
15print(m(x, training=False))

That training value is propagated through the managed call path. Writing code in the normal model(x, training=...) style keeps it aligned with Keras expectations.

First Call Also Builds the Model

Subclassed models often create weights lazily because input shape is not always known in __init__.

python
1model = MyModel()
2print(model.built)
3
4_ = model(tf.zeros((1, 3)))
5print(model.built)

That first object call is the normal moment when variables are created. If you bypass the intended call path too aggressively, debugging build-time behavior becomes harder.

Best Practice for Subclassed Models

The standard pattern is:

  1. Define layers in __init__.
  2. Put forward-pass logic in call.
  3. Execute with model(x), not model.call(x).

That division keeps the code compatible with fit, predict, evaluate, tracing, and export.

Common Pitfalls

  • Assuming call is not executed because user code writes model(x). Fix by remembering __call__ delegates to call.
  • Invoking model.call(...) directly as the main API. Fix by using model(...) so Keras wrapper behavior stays active.
  • Forgetting to include training=False in the custom call signature when the model uses training-sensitive layers. Fix by adding the parameter explicitly.
  • Creating new layers inside call instead of __init__. Fix by constructing layers once in __init__ so variables are not recreated on every pass.
  • Mixing ad hoc manual invocation style with fit and predict expectations. Fix by following one consistent subclassing pattern everywhere.

Summary

  • In Keras subclassing, model(x) is the correct public way to run the model.
  • 'call is still executed, but it is reached through __call__.'
  • The wrapper adds important framework behavior beyond raw forward computation.
  • Direct call(...) can bypass masking, tracing, and lifecycle logic.
  • Keep layers in __init__, forward logic in call, and execution through model(...).

Course illustration
Course illustration

All Rights Reserved.