Python does it right. I've never bought the argument that "exceptions are for exceptional circumstances". If you use error codes for some errors and exceptions for others, all you do is confuse people. The definition of "exceptional error" is subjective, after all.
As far as I can tell, the advice to use "exceptions for exceptional circumstances" originated in C++ circles in the 1990s, when support for exceptions was bad and throws were slow. The thinking was that you could efficiently use error codes for commonly-encountered errors and still get some of the syntactic benefits of using exceptions for rarer errors. Also, if you were using a compiler without exceptions support, you could turn your exceptions into aborts while not aborting just because a file doesn't exist. You can see vestiges of this thinking in the iostreams API.
Somehow, this performance hack morphed into commonly-accepted dogma. I don't accept this dogma. Exceptions are for any situation that a function can't meet its contract. In Python, if an iterator can't supply its next element, it can't satisfy the contract, and the exception tells you why. I don't think there's anything particularly wrong with that.
(The irony in all this is that Rust's approach can be interpreted as an attempt to return exceptions to their roots by relegating unwinding to exceptional circumstances, as opposed to the routine error-handling solution that they've become in languages that have chosen to embrace exceptions (e.g. Python's egregious `StopIteration` exception for loop termination).)