Three containers, one pattern
PyTorch's three core composition modules:
nn.Sequential(*modules)— runs modules in order, output of one feeds the next. No customforward()needed.nn.ModuleList([modules])— a Python list that PyTorch can see. You write your ownforward()and iterate however you want.nn.ModuleDict({name: module})— same idea but dict-shaped. Useful for multi-head models or branching architectures.
The "use Sequential when possible, ModuleList when you have to" rule is right for ~80% of code. Sequential reads cleanly and shows up nicely in print(model). ModuleList is for anything with branches, skip connections, or dynamic depth (e.g., a Transformer with N layers where N comes from a config).
The footgun: regular Python collections don't work
If you store layers in a plain list or dict (not the nn versions), they will not appear in model.parameters(), not move with .to(device), and not be saved by state_dict(). The bug is silent — your model trains, but only the layers PyTorch can see. Always use the nn.Module* versions.