Title: Python skips __next__ directive and returns a generator object
Tags: python,iterator,python-3.8

Question: I'm trying to implement an iterable class, which I have done several times before, but I'm experiencing some unexpected behavior this time around, and I can't figure out why.
My class contains the usual __iter__(self) method that returns self, and __next__(self) method that yields results, however, when I attempt to do the following:
with VSIFile(params) as vsi:
  for roi in vsi:
     print(roi)

The roi is in fact a generator object instead of a yielded result. After going into debug, I found that __next__ never triggers, only __iter__. I tested making an iterator with a simple number counting class and that one works well.
I expect roi to be a numpy array.
Here's the full code:
vsi_file.py
from typing import Tuple
import javabridge
import bioformats
from tqdm import tqdm
from cv2 import resize

javabridge.start_vm(class_path=bioformats.JARS)


class VSIFile:

    def __init__(self, vsi_file: str,
                 roi_size: Tuple[int, int] = (1024, 1024),
                 target_size: Tuple[int, int] = (256, 256),
                 use_pbar: bool = True):

        self.file_path = vsi_file
        self.roi_size = roi_size
        self.target_size = target_size
        self.slide = None
        self.shape = None
        self.max_x_idx = None
        self.max_y_idx = None
        self.num_rois = None
        self.skip = [1, 2, 5, 11, 22, 45, 72]

        if use_pbar:
            self.pbar = tqdm()
        else:
            self.pbar = None

    def __enter__(self):
        self.slide = bioformats.ImageReader(self.file_path)
        self.shape = self.slide.rdr.getSizeY(), self.slide.rdr.getSizeX(), 3
        self.max_x_idx = self.shape[1] // self.roi_size[1]
        self.max_y_idx = self.shape[0] // self.roi_size[0]
        self.num_rois = self.max_x_idx * self.max_y_idx

        if self.pbar is not None:
            self.pbar.total = self.num_rois
            self.pbar.refresh()

        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.slide.close()

        if self.pbar is not None:
            self.pbar.close()

    def __del__(self):
        self.slide.close()

        if self.pbar is not None:
            self.pbar.close()

    def __iter__(self):
        return self

    def __next__(self):
        while self.idx in self.skip:
            if self.idx == self.max_x_idx * self.max_y_idx:
                if self.pbar is not None:
                    self.pbar.close()
                raise StopIteration

            self.idx += 1

            if self.pbar is not None:
                self.pbar.update(1)

        if self.idx == self.max_x_idx * self.max_y_idx:
            if self.pbar is not None:
                self.pbar.close()
            raise StopIteration

        y = (self.idx // self.max_x_idx) * self.roi_size[0]
        x = (self.idx % self.max_x_idx) * self.roi_size[1]

        roi = self.get_roi(x, y, self.roi_size[0], self.roi_size[1])
        roi = resize(roi, self.target_size) if self.target_size else roi
        yield roi

        self.idx += 1

        if self.pbar is not None:
            self.pbar.update(1)


process_vsi.py (relevant portion)
with VSIFile(os.path.abspath(os.path.join(data_dir, file))) as vsi:
  for roi in vsi:
    print(roi)

This prints <generator object VSIFile.__next__ at 0x000001AA3C4AFC80>.

Answer: You're mixing two paradigms of how you could implement an iterator.
When you implement __next__, it is supposed to return its elements, not yield them.
If you want to use yield, do that directly inside __iter__.