@graham_knapp
A lot of it is always going to come down to personal preferences. But I think the different cases are more or less appropriate depending on the problem being solved.
If you're calling a function or creating an object that you expect to almost-always work - it fails when low on memory, or when the network connection is broken, or some other exceptional (hint) condition, then I think exceptions are most appropriate. Throw one, and let it bubble up the caller's stack until they either handle it and possibly re-try, or the program exits with a diagnostic.
If you're calling something that can reasonably be expected to both succeed or fail under normal conditions - say a command given in an inappropriate state fails, but otherwise succeeds, maybe - then an explicit return object with success/failure (and possibly a result, and / or an error message) can be better, as the caller expects to handle failures immediately.
Both are easy in Python; I've implemented the latter with tuples, named tuples, custom classes, and dataclasses over the years for different projects.
</0.02>