No problem mate, we have to begin from somewhere but we finally learn!
Yes, in the end, all modules need tensor input. So, you are right, somehow, if you want to use few functions, you have convert to PIL and then to Tensor. For instance, assume these transforms:
custom_transforms = Compose([
    RandomResizedCrop(size=224, scale=(0.8, 1.2)),
    RandomRotation(degrees=(-30, 30)),
    RandomHorizontalFlip(p=0.5),
    ToTensor(),
    Normalize(mean=mean, std=std),
    RandomNoise(p=0.5, mean=0, std=0.1)])


Note that I had to use ToTensor if I wanted to use built-in Normalize. Although, the last transformation RandomNoise is not part of PyTorch and I have implemented it, but still you can define its inputs so it can work with tensors (in my example) or PIL image.
class RandomNoise(object):
    def __init__(self, p, mean=0, std=0.1):
        self.p = p
        self.mean = mean
        self.std = std

    def __call__(self, img):
        if random.random() <= self.p:
            noise = torch.empty(*img.size(), dtype=torch.float, requires_grad=False)
            return img+noise.normal_(self.mean, self.std)
        return img

On top of that, all modules in PyTorch are available as a function under torch.nn.functional so for instance if you want the class type, use transforms.ToTensor but if you need it as a function in any place of your code, just use functional.to_tensor().
That’s why I love PyTorch, really easy to read and use, as you said, like a charm!
If you are new to PyTorch, I highly suggest you to read all tutorial in official website/github. It won’t take much time but will enable you to understand the whole idea easily.
bests