Is Python more suited for EAFP1 or LBYL2 coding styles? It has been debated across message boards, email chains, stackoverflow, twitter and workplaces. It’s not as heated as some other battles; like how spaces are better than tabs, or that nano is indisputably the best terminal editor 3; but people seem to have their own preference on the subject. What many will gloss over is the fact that Python as a language doesn’t have a preference. Rather, different design patterns lend themselves better to one or the other.
I disagree with the position that EAFP is better than LBYL, or “generally recommended” by Python – Guido van Rossum 4
If you haven’t already, I suggest taking a look at Brett Cannon’s blog post about how EAFP is a valid method with Python that programmers from other languages may not be familiar with. The benefits are EAFP are simple: Explicit code, fail fast, succeed faster, and DRY (don’t repeat yourself). In general I find myself using it more than LBYL, but that doesn’t mean it’s always the right way.
Use Case Dependent
First, a little code comparison to see the differences between them. Lets try to work with a list of list of strings. Our goal is that if the inner list is at least three items long, copy the third item into another list.
messages = [["hi there", "how are you?", "I'm doing fine"]] out = [] # LBYL if messages and messages[0] and len(messages[0]) >= 3: out.append(messages[0][2]) # EAFP try: out.append(messages[0][2]) except IndexError as err: print(err) # In the real world, this should be a logging `.exception()`
Both pieces of code are short and easy to follow what they are accomplishing. However, when there are at least three items in the inner list, LBYL will take almost twice the amount of time! When there aren’t any errors happening, the checks are just additional weight slowing the process down.
But if we prune the messages down to [["hi there", "how are you?"]]
then EAFP will be much slower, because it will always try a bad lookup, fail and then have to catch that failure. Whereas LBYL will only perform the check, see it can’t do it, and move on. Check out my speed comparison gist yourself.
Some people might think putting both the append and lookup in the same try block is wrong. However, it is both faster; as we only have to do the lookup once; and the proper way; as we are only catching the possible IndexError
and not anything that would arise from the append.
The real takeaway from this is to know which side of the coin you area dealing with beforehand. Is your incoming data almost always going to be correct? Then I’d lean towards EAFP, whereas if it’s a crap shoot, LBYL may make more sense.
Sometimes one is always faster
There are some instances when one is almost always faster than the other (though speed isn’t everything). For example, file operations are faster with EAFP because the less IO wait, the better.
Lets try making a directory when we are not sure if it was created yet or not, while pretending it’s still ye olde times and os.makedirs(..., exist_ok=True)
doesn’t exist yet.
import os # LBYL if not os.path.exists("folder"): os.mkdir("folder") # EAFP try: os.mkdir("folder") except FileExistsError: pass
In this case, it’s always preferential to use EAFP, as it’s faster (even when executed on a m.2 SSD) , there are no side effects, and the error is highly specific so there is no need for special handling.
Be careful though, many times when dealing with files you don’t want to leave something in a bad state, in those cases, you would use LBYL.
What bad things can happen when you don’t ask permission?
Side effects
In many cases, if something fails to execute, the state of the program or associated files might have changed. If it’s not easy to revert to a known good state in the exception clause, EAFP should be avoided.
# DO NOT DO with open("file.txt", "w") as f: try: f.write("Hi there!\n") f.write(message[3]) except IndexError: pass # Don't do, need to be able to get file back to original state
Catching too much
If you are wrapping something with except Exception
or worse, the dreaded blank except:
, then you shouldn’t be using EAFP (if you’re using black except:
still, you need to read up more on that and stop it!)
# DO NOT DO try: do_something() except: # If others see this you will be reported to the Python Secret Police pass
Also, sometimes errors seem specific, like an OSError, but could raise multiple different child errors that must be parsed through.
# DO import errno try: os.scandir("secret_files") except FileNotFoundError: # A child of OSError # could be put as 'elif err.errno == errno.ENOENT' below log.error("Yo dawg, that directory doesn't exist, " "we'll try the backup folder") except OSError as err: if err.errno in (errno.EACCES, errno.EPERM): log.error("We don't have permission to access that capt'n!" "We'll try the backup folder") else: log.exception(f"Unexpected OSError: {err}") raise err # If you don't expect an error, don't keep going. # This isn't Javascript
When to use what?
EAFP (Easier to Ask for Forgiveness than Permission)
- IO operations (Hard drive and Networking)
- Actions that will almost always be successful
- Database operations (when dealing with transactions and can rollback)
- Fast prototyping in a throw away environment
LBYL (Look Before You Leap):
- Irrevocable actions, or anything that may have a side effect
- Operation that may fail more times than succeed
- When an exception that needs special attention could be easily caught beforehand
Sometime you might even find yourself using both for complex or mission critical applications. Like programming the space shuttle, or getting your code above that 500 lines the teacher wants.