The best-of-both-worlds pattern
This is the move the whole track has been building toward: subclass a custom layer, then compose it with the Functional API. The layer holds whatever Python logic you need on the inside; from the outside, it's a callable that takes a tensor and returns a tensor — indistinguishable from Dense or Conv2D. So you call it inside a Functional graph exactly like a built-in (see the Code block).
Why the seam works — and why it's free
The reason this composes cleanly is the layer contract. Every Keras layer, built-in or custom, exposes the same interface: layer(tensor) -> tensor. The Functional API doesn't care what happens inside a layer — it only wires the tensor that comes out into the next call. So your ResidualBlock, with its skip connection and Python-level logic, slots into the graph and the surrounding Functional structure stays fully introspectable: model.summary() still draws the skeleton, and only the custom block's internals are opaque. You localize the introspection cost from Lesson 1 to exactly the blocks that earned it.
The reverse direction works too: a subclassed Model can build a Functional sub-model in its __init__() and call it from call(). In practice the common shape is a pretrained Functional backbone (ResNet, a vision transformer) wrapped by a thin subclassed head that does the custom routing or loss bookkeeping.