We can use a generator to do this. We make an in order traversal iterator and then call next on it.

class BSTIterator:
    def __init__(self, root: 'Optional[TreeNode]'):
        def inorder(node):
            if node:
                yield from inorder(node.left)
                yield node.val
                yield from inorder(node.right)
 
        # store the generator
        self.gen = inorder(root)
        self.buffer = None  # lookahead buffer
 
    def next(self) -> int:
        # if we already peeked, consume it
        if self.buffer is not None:
            val, self.buffer = self.buffer, None
            return val
        return next(self.gen)
 
    def hasNext(self) -> bool:
        if self.buffer is not None:
            return True
        try:
            self.buffer = next(self.gen)
            return True
        except StopIteration:
            return False

You can use the stack explicitly as well:

class BSTIterator:
    def __init__(self, root: Optional[TreeNode]):
        self.iter = self._inorder(root)
        self.nxt = next(self.iter, None)
 
    def _inorder(self, node: Optional[TreeNode]) -> Generator[int, None, None]:
        stack = []
        while node or stack:
            while node:
                stack.append(node)
                node = node.left
            node = stack.pop()
            yield node.val
            node = node.right
 
    def next(self) -> int:
        res = self.nxt
        self.nxt = next(self.iter, None)
        return res
 
    def hasNext(self) -> bool:
        return self.nxt is not None