Why a GAN breaks the single-loss mold
A GAN is two networks locked in a minimax game. The discriminator learns to tell real images from generated ones; the generator learns to produce fakes the discriminator can't catch. They have opposing objectives, separate weights, and separate optimizers — so the single loss, single optimizer shape that fit() assumes simply can't express them. Each batch needs two distinct update steps in sequence.
Why train_step() still fits
It's tempting to reach straight for a manual loop here, but the alternating pattern lives perfectly inside a train_step() override. You hold both sub-models on the GAN object, and inside one step you train the discriminator on a real+fake batch, then train the generator against frozen-discriminator feedback, and return both losses. Because you stayed inside fit(), the whole GAN inherits callbacks, checkpointing, and distribution for free. The skeleton below keeps the two updates in named helper methods (_train_discriminator / _train_generator) so the step reads as the game it models; each helper holds its own gradient tape and optimizer.