You could filter out the batch norm layers and set the requires_grad attribute of all other parameters to False:
model = models.resnet18()

def freeze_all_but_bn(m):
    if not isinstance(m, torch.nn.modules.batchnorm._BatchNorm):
        if hasattr(m, 'weight') and m.weight is not None:
            m.weight.requires_grad_(False)
        if hasattr(m, 'bias') and m.bias is not None:
            m.bias.requires_grad_(False)
              
model.apply(freeze_all_but_bn)
print(model.fc.weight.requires_grad)
> False
print(model.layer4[1].bn2.weight.requires_grad)
> True

This code should make sure, that no BatchNorm layer will accidentally be manipulated in the recursive function.