The recommendation in Python is to use exceptions to indicate failure. This is true even if you expect failure on a regular basis.
Look at it from the perspective of the caller of your code:
my_status = get_abe_status(my_url)
What if we return None? If the caller doesn't specifically handle the case that get_abe_status failed, it will simply try to continue on with my_stats being None. That may produce a difficult to diagnose bug later on. Even if you do check for None, this code has no clue why get_abe_status() failed.
But what if we raise an exception? If the caller doesn't specifically handle the case, the exception will propagate upward eventually hitting the default exception handler. That may not be the what you want, but its better then introducing a subtle bug elsewhere in the program. Additionally, the exception gives information about what went wrong which is lost in the first version.
From the caller's perspective, its simply more convenient to get an exception than a return value. And that's the python style, to use exceptions to indicate failure conditions not return values.
Some will take a different perspective and argue that you should only use exceptions for cases you never really expect to happen. They argue that normally running running could should not raise any exceptions. One reason that is given for this is that exceptions are grossly inefficient, but that's not actually true for Python.
A couple of points on your code:
try:
hits[0]
except IndexError:
raise NotFoundError("No mentions found.")
That's a really confusing way to check for an empty list. Don't induce an exception just to check something. Use an if.
# say we expect four hits...
if len(hits) != 4:
raise Warning("An unexpected number of hits.")
logger.warning("An unexpected number of hits.")
You do realize that the logger.warning line will never run right?
You can pattern-match on a specific exception type like this:
testAddress :: HostName -> Int -> IO (Maybe Handle)
testAddress host iPort = do
let port = PortNumber $ fromIntegral iPort
putStrLn $ "Testing - Host: " ++ host ++ ", Port: " ++ show iPort
result <- try $ connectTo host port
case result of
Left (e :: MyExceptionType) -> return Nothing
Right h -> do
putStrLn $ "Connected to " ++ host
return $ Just h
This will actually cause Haskell to infer that the type of result
is Either MyExceptionType Handle
, and try
will therefore only catch exceptions of MyExceptionType
, propagating all others unchanged.
As for the particular exception types used by the network
package, I couldn't find any documentation on them either, but since Typeable
is a superclass of Exception
, you should be able to print out the type of any exception you catch, which should be useful.
Best Answer
This is a legitimate criticism of exceptions. They are often less visible than simple error handling such as returning a code. And there is no easy way to enforce a "contract". Part of the point is to enable you to let exceptions be caught at a higher level (if you have to catch every exception at every level, how different is it from returning an error code, anyway?). And this means that your code could be called by some other code that doesn't handle it appropriately.
Exceptions do have downsides; you have to make a case based on cost-benefit.
I found these two articles helpful: The necessity of exceptions and Everything wrong with exceptions. Also, this blog post offers opinions of many experts on exceptions, with a focus on C++. While expert opinion seems to lean in favor of exceptions, it is far from a clear consensus.
As for convincing your team lead, this might not be the right battle to pick. Especially not with legacy code. As noted in the second link above:
Adding a little bit of code which uses exceptions to a project that mainly does not is probably not going to be an improvement. Not using exceptions in otherwise well-written code is far from a catastrophic problem; it might not be a problem at all, depending on the application and which expert you ask. You have to pick your battles.
This is probably not an argument I would spend effort on--at least not until a new project is started. And even if you have a new project, is it going to use or be used by any legacy code?