call() is the contract
Everything a layer does lives in call(). It takes the input tensor and returns the output tensor, and the signature carries two arguments Keras passes in automatically when they apply:
training— a boolean that flips behavior between train and inference. Dropout drops units only whentraining=True; BatchNormalization uses the current batch's statistics during training but its accumulated moving statistics during inference. If your layer wraps either, you must accept this flag and pass it down.mask— an optional boolean tensor that marks which timesteps are real versus padding, so attention and recurrent layers can ignore the padded positions.
This is where the Python freedom pays off
Because call() is just a method, you can put real control flow inside it — an if training: branch, a loop, a shape-dependent computation. That's the whole reason to subclass instead of using the Functional API (see the Code block). The one discipline that keeps that freedom portable: do your tensor math with keras.ops, never with a backend's native ops. Write keras.ops.relu(x) and the same layer runs on TensorFlow, PyTorch, and JAX unchanged.