100

What's better practice in a user-defined function in Python: raise an exception or return None? For example, I have a function that finds the most recent file in a folder.

def latestpdf(folder):
    # list the files and sort them
    try:
        latest = files[-1]
    except IndexError:
        # Folder is empty.
        return None  # One possibility
        raise FileNotFoundError()  # Alternative
    else:
        return somefunc(latest)  # In my case, somefunc parses the filename

Another option is leave the exception and handle it in the caller code, but I figure it's more clear to deal with a FileNotFoundError than an IndexError. Or is it bad form to re-raise an exception with a different name?

martineau
  • 119,623
  • 25
  • 170
  • 301
parcydarks
  • 1,015
  • 2
  • 8
  • 6
  • Similar: http://stackoverflow.com/questions/1152541/is-it-better-to-use-exception-or-return-code-in-python – codeape Aug 21 '09 at 20:22
  • 3
    I lean towards raising an exception so I am forced to handle the exception in the calling function. If I forget to check if the output is None in the calling function, I could have a latent bug. If you returned None, hopefully the next line in the calling function will raise an AttributeError. However if the returned value is added to a dictionary and then 100 function calls in a different source file an AttributeError is raised, you'll have fun hunting down why that value was None. – IceArdor Jul 14 '14 at 06:05
  • In general, I also avoid values that have a special meaning or having multiple signatures for one function (it could return a string or None). – IceArdor Jul 14 '14 at 06:08

5 Answers5

104

It's really a matter of semantics. What does foo = latestpdf(d) mean?

Is it perfectly reasonable that there's no latest file? Then sure, just return None.

Are you expecting to always find a latest file? Raise an exception. And yes, re-raising a more appropriate exception is fine.

If this is just a general function that's supposed to apply to any directory, I'd do the former and return None. If the directory is, e.g., meant to be a specific data directory that contains an application's known set of files, I'd raise an exception.

Neuron
  • 5,141
  • 5
  • 38
  • 59
Eevee
  • 47,412
  • 11
  • 95
  • 127
  • 4
    Another point to consider: If raising an exception a message can be attached, but we cannot do that when returning `None`. – kawing-chiu Jan 19 '17 at 06:08
  • Also, raising and handling exceptions is slower: https://stackoverflow.com/a/17359901/1116842 – moi Mar 02 '21 at 09:59
  • @moi on the order of _microseconds_, a difference which isn't meaningful for a single function that seems unlikely to be called very often. (and that test isn't even very good; the exception path constructs a new object, whereas the return path does not.) – Eevee Mar 13 '21 at 21:51
  • In this specific case, this is right. My comment was more about the more general case where a function might as well be part of a critical path. Then, these microseconds matter a lot. (And yes, throwing an exception constructs a new object, namely the exception itself. This takes time.) – moi Mar 15 '21 at 07:33
  • A function like `def a(): pass` takes 75ns on my machine. A call to `def b(): try: raise ValueError(): except ValueError: pass` takes 255ns. A call to `def c(): try: pass except ValueError: pass` takes 83ns. If you expect exceptions to be rare, the overhead will be negligible. If the exceptions will be common, you can check before to try to avoid the exception, but likely the overhead of the function itself is already comparable to your exception-handling overhead and is a bigger issue for your critical path. – chepner Feb 11 '22 at 15:28
8

I would make a couple suggestions before answering your question as it may answer the question for you.

  • Always name your functions descriptive. latestpdf means very little to anyone but looking over your function latestpdf() gets the latest pdf. I would suggest that you name it getLatestPdfFromFolder(folder).

As soon as I did this it became clear what it should return.. If there isn't a pdf raise an exception. But wait there more..

  • Keep the functions clearly defined. Since it's not apparent what somefuc is supposed to do and it's not (apparently) obvious how it relates to getting the latest pdf I would suggest you move it out. This makes the code much more readable.

for folder in folders:
   try:
       latest = getLatestPdfFromFolder(folder)
       results = somefuc(latest)
   except IOError: pass

Hope this helps!

rh0dium
  • 6,811
  • 4
  • 46
  • 79
  • 8
    Or `get_latest_pdf_from_folder`. Indeed, Pep8: "Function names should be lowercase, with words separated by underscores as necessary to improve readability." – PatrickT Jun 13 '20 at 10:09
6

I usually prefer to handle exceptions internally (i.e. try/except inside the called function, possibly returning a None) because python is dynamically typed. In general, I consider it a judgment call one way or the other, but in a dynamically typed language, there are small factors that tip the scales in favor of not passing the exception to the caller:

  1. Anyone calling your function is not notified of the exceptions that can be thrown. It becomes a bit of an art form to know what kind of exception you are hunting for (and generic except blocks ought to be avoided).
  2. if val is None is a little easier than except ComplicatedCustomExceptionThatHadToBeImportedFromSomeNameSpace. Seriously, I hate having to remember to type from django.core.exceptions import ObjectDoesNotExist at the top of all my django files just to handle a really common use case. In a statically typed world, let the editor do it for you.

Honestly, though, it's always a judgment call, and the situation you're describing, where the called function receives an error it can't help, is an excellent reason to re-raise an exception that is meaningful. You have the exact right idea, but unless you're exception is going to provide more meaningful information in a stack trace than

AttributeError: 'NoneType' object has no attribute 'foo'

which, nine times out of ten, is what the caller will see if you return an unhandled None, don't bother.

(All this kind of makes me wish that python exceptions had the cause attributes by default, as in java, which lets you pass exceptions into new exceptions so that you can rethrow all you want and never lose the original source of the problem.)

David Berger
  • 12,385
  • 6
  • 38
  • 51
  • The argument that the possible exceptions are not defined and so it is hard to catch is very valid argument for Python. – snorberhuis Jan 17 '20 at 12:57
  • You can pass exceptions into new exceptions (and it is done by default if you raise a new exception while handling an existing one): `except exc: raise NewException(...) from exc`. – moi Mar 02 '21 at 09:58
6

with python 3.5's typing:

example function when returning None will be:

def latestpdf(folder: str) -> Union[str, None]

and when raising an exception will be:

def latestpdf(folder: str) -> str 

option 2 seem more readable and pythonic

(+option to add comment to exception as stated earlier.)

Asaf
  • 65
  • 1
  • 6
  • 8
    `Union[str, None]` should be `Optional[str]` – Georgy Dec 25 '18 at 12:54
  • 2
    a shorthand, but you're right, it's more readable. not editing so both options are here. – Asaf Dec 27 '18 at 18:56
  • 2
    2 is potentially more readable but (unfortunately?) type hints don't indicate an exception could be thrown. I've lately found that 1 will help catch more errors as you are forced to handle a None return. – jonespm Jan 15 '20 at 13:34
2

In general, I'd say an exception should be thrown if something catastrophic has occured that cannot be recovered from (i.e. your function deals with some internet resource that cannot be connected to), and you should return None if your function should really return something but nothing would be appropriate to return (i.e. "None" if your function tries to match a substring in a string for example).

  • [str.find](https://docs.python.org/3/library/stdtypes.html#str.find) is an example for this behavior: It returns -1 if the substring was not found. However this is a direct translation of the C implementation. [list.index](https://docs.python.org/3/library/stdtypes.html#common-sequence-operations) is a counter-example: It raises a `ValueError` if the value was not found. – moi Mar 02 '21 at 09:56