Keras 3's headline trick: pick your backend at runtime via one env var. TensorFlow, PyTorch, or JAX — same model = keras.Sequential(...) line, same .fit(). This track teaches the mental model behind that, the gotchas (mixing tensors across backends), and the keras.ops namespace that lets you write backend-agnostic custom math.
Description vs. execution
The key insight behind Keras 3 is separating model description from execution. When you define layers, connect them, and specify training — you're describing what to compute. The backend decides how to compute it. That line — between intent and implementation — is the whole reason one model file can run on three engines.
This isn't a new idea in software, but it's unusually hard to pull off for deep learning, where the engine owns autodiff, device placement, kernel selection, and graph compilation. Most "portable" frameworks fake it by reimplementing the engine; Keras 3 instead routes every primitive op down to whichever engine is loaded.
The dispatch layer
That routing happens through keras.ops, which dispatches each operation to the active backend. Under the hood:
keras.ops.matmul(a, b)→jax.numpy.matmul/tf.matmul/torch.matmulkeras.ops.nn.softmax(x)→jax.nn.softmax/tf.nn.softmax/torch.nn.functional.softmax
Because every built-in layer, loss, metric, and optimizer is written against keras.ops, they inherit portability for free — you never opt into it.
Why the saved file is engine-free
KerasTensors are backend-agnostic symbolic tensors used during model construction. They describe the computation graph without committing to any framework — which is exactly why a saved .keras file contains no backend-specific operations. Train under JAX today, hand the file to a teammate running TensorFlow tomorrow, and it just loads. That's the payoff the next lessons build on.