“The Python SPAKE2 code has a bunch of assertions to make sure that one method isn’t called before another”
This is not a good OOP design, it has nothing to do with Python per se. Yes, in Haskell you use types that can enforce the state transition. In OOP you use objects, not a 100% isomorphism with the type-based solution, but simple, clear, and effective in an OOP setting.
I don’t think that’s true; the essence of OOP is that you have objects that you send messages to and have no visibility of their internal state, which means there’s always the possibility of sending the wrong kind of message for the current state. Haskell-style programming really does eliminate a broad class of potential errors here.
Trivially, if something must be started before it can be finished, have the start message take a function whose argument is an object that can receive a finish message. That’s just continuation passing to linearize starting before finishing.
s start: [ :f | f finish ]
At which point you’ve shifted away from OO style and into a more functional style.
Smalltalk has had code along these lines for more than 40 years.
That’s just one way I chose to appeal to an FP frame of mind. Here’s another…
f := s start.