Many of the problems that people who eschew exceptions are trying to avoid or fix, Python simply shrugs and declares isn't a problem. If the software you're writing agrees with that assertion (which it probably does if you're writing Python with no static type annotations and no pass through mypy or a similar tool to verify those type annotations are sound), you're fine.
Unfortunately, when you do eventually find yourself in a place where you have to care (which is the fate of any codebase that becomes large or complex enough), Python actively hinders making reliable code that is easy for strangers to modify without introducing subtle bugs.
I often fall into the "Python trap" (you'd think I would have learned by now!): "this is a tiny project, almost a script, and it's so much nicer and faster to code it in Python. And since it's so small, who cares about static typing? Surely this won't grow larger". A couple of months later: "oh, no!" [1]
[1] If you know the webcomic "webcomic name" by Alex Norris, it fits perfectly here.
Python's soft static type rules and extremely flexible variable instantiation unfortunately turns every line of code into a potential runtime error, because the code can't know at compile time if a variable was introduced to the global context that could bind to any given name. Typos are therefore deadly at runtime in Python in a way that they fundamentally aren't in other languages; if you try to write
foo = 0
if foh == 1: # oh no, I misspelled the variable
... many languages (including F#) will fail to compile the program because 'foh is uninitialized.' Python can't know if 'foh' is intended to be a global variable and so will execute the program and only determine while evaluating that line that 'foh' doesn't exist. This is especially insidious in error handling, where the error codepath isn't necessarily exercised; essentially, Python's flexibility implies that if your unit tests don't have 100% line coverage, you can't even know if your program is basically devoid of simple variable typos naming never-existing variables (a check most languages give you for free).
In a language with that feature, a rich ecosystem of exceptions is almost necessary, because every line of code could hide a runtime exception!
Maybe I am missing something but I think every dynamic typing system has this problem. I can't see what this has to do with Python and exceptions in general. It is not like there aren't statically typed languages with exceptions. This comment is an excellent critique of dynamic typed systems but it has nothing to do with exceptions.
The top-thread post was referencing Python, so I'm speaking in the Python domain specifically. Other dynamic languages have this problem also, and other languages with stronger static type guarantees do support exceptions.
I haven't encountered a language with dynamic typing of the sort Python has that doesn't also have a robust runtime exception system, and it'd be interesting to see what that looks like. There's probably some old flavors of BASIC that fit that mold (i.e. variable declaration is not required and also the only thing it offers for exception handling is setting a label to GOTO if a runtime exception occurs).
I guess I was too fixated on the exceptions part of your comment. You are right that due to Python's dynamic nature every line can turn into a runtime exception. Maybe it's because English is my second language but I couldn't understand that in your first comment.
Exceptions for control flow is bad, IF that control flow is an expected business rule.
Exceptions shoudl be exceptions. And python programmers often write a whole exception class model to define all business logic rules... Really use Enum for that!
If the object isn't found, the caller gets a 404 response without the person writing the view having to do a single thing. However, they can still handle the problem themselves if they really want to:
I absolutely love this coding style because exceptions are still being handled everywhere, but don't have to be explicitly dealt with deep inside a nested call stack. That lets us write very uncluttered, testable view code like:
def change_password(userid, oldpass, newpass):
# This raises an exception if the user can't be found
user = get_user_by_id(userid)
# This raises an exception if the old password is wrong
verify_password(user, oldpass)
# This raises an exception if the DB couldn't be updated,
# perhaps because of a race condition with another request
update_password(user, newpass)
# By the time we get to this line, everything above has to have
# succeeded, with zero manual error checking inside this view
return {"result": "Password successfully updated."}
Having worked in Twisted's framework, I utterly despise exceptions as control flow. One service I work in is 140k lines of Twisted python (aptly named). I can't wait for us to finally kill it and never deal with Twisted again.
I also use exceptions for control-flow and I feel no shame about it. It's often the best (most concise, most performant, most understandable) solution to a problem, in my eyes.
Most concise: absolutely. Most performant: never. Most understandable: yes, but only if you are cognizant of which call paths can result in an exception and which can’t (i.e. until your code base becomes too large or you’re not the one that wrote it).
Most exceptions, at least in the JVM where this is commonly debated, will incur a penalty of building up the exception and unwinding the call stack. What aspect of performance are you using as an example?
An exception thrown and caught within a compilation unit is effectively a goto. It can translate into nothing more than a jcc instruction, or even nothing at all if it's unconditional. This can be higher performance in practice than threading through a set of Either-style return objects.
You are probably looking for continuations. For many languages exceptions are the nearest approximation.
Personally I have no problem with your approach on the semantic level, but most compiler/interpreter developers consider all exceptional paths as cold as it gets: performance can be "interesting" if you take a lot of them.
Unfortunately, when you do eventually find yourself in a place where you have to care (which is the fate of any codebase that becomes large or complex enough), Python actively hinders making reliable code that is easy for strangers to modify without introducing subtle bugs.