Dealing with errors and exceptional situations is a common requirement in programming. You can either prevent errors before they happen or handle errors after they’ve happened. In general, you’ll have two coding styles matching these strategies: look before you leap (LBYL), and easier to ask forgiveness than permission (EAFP), respectively. In this tutorial, you’ll dive into the questions and considerations surrounding LBYL vs EAFP in Python.
By learning about Python’s LBYL and EAFP coding styles, you’ll be able to decide which strategy and coding style to use when you’re dealing with errors in your code.
In this tutorial, you’ll learn how to:
- Use the LBYL and EAFP styles in your Python code
- Understand the pros and cons of LBYL vs EAFP
- Decide when to use either LBYL or EAFP
To get the most out of this tutorial, you should be familiar with how conditional statements and try
… except
statements work. These two statements are the building blocks for implementing the LBYL and EAFP coding styles in Python.
Free Bonus: 5 Thoughts On Python Mastery, a free course for Python developers that shows you the roadmap and the mindset you’ll need to take your Python skills to the next level.
Errors and Exceptional Situations: Preventing or Handling Them?
Dealing with errors and exceptional situations is a fundamental part of computer programming. Errors and exceptions are everywhere, and you need to learn how to manage them if you want robust and reliable code.
You can follow at least two general strategies when it comes to dealing with errors and exceptions:
- Prevent errors or exceptional situations from happening
- Handle errors or exceptional situations after they happen
Historically, preventing errors before they happen has been the most common strategy or approach in programming. This approach typically relies on conditional statements, also known as if
statements in many programming languages.
Handling errors and exceptions after they’ve happened came onto the scene when programming languages started to provide exception-handling mechanisms, such as try
… catch
statements in Java and C++, and try
… except
statements in Python. However, in Java and C++, handling exceptions can be a costly operation, so these languages tend to prevent errors rather than handling them.
Note: One optimization coming in Python 3.11 is zero-cost exceptions. This implies that the cost of try
statements will be almost eliminated when no exception is raised.
Other programming languages like C and Go don’t even have exception-handling mechanisms. So, for example, Go programmers are used to preventing errors using conditional statements, like in the following example:
func SomeFunc(arg int) error {
result, err := DoSomething(arg)
if err != nil {
// Handle the error here...
log.Print(err)
return err
}
return nil
}
This hypothetical Go function calls DoSomething()
and stores its return values in result
and err
. The err
variable will hold any error that occurs during the function execution. If no error occurs, then err
will contain nil
, which is the null value in Go.
Then the if
statement checks if the error is different from nil
, in which case, the function proceeds to handle the error. This pattern is pretty common, and you’ll see it repeatedly in most Go programs.
Python’s exception-handling mechanisms are pretty efficient when no exception is raised. Therefore, in Python, it’s common and sometimes encouraged to deal with errors and exceptional situations using the language’s exception-handling syntax. This practice often surprises people who come from other programming languages.
What this means for you is that Python is flexible and efficient enough that you can select the right strategy to deal with errors and exceptional situations in your code. You can either prevent errors with conditional statements or handle errors with try
… except
statements.
Pythonistas typically use the following terminology to identify these two strategies for dealing with errors and exceptional situations:
Strategy | Terminology |
---|---|
Preventing errors from occurring | Look before you leap (LBYL) |
Handling errors after they occur | Easier to ask forgiveness than permission (EAFP) |
In the following sections, you’ll learn about these two strategies, also known as coding styles in Python and other programming languages.
Programming languages with costly exception-handling mechanisms tend to rely on checking for possible errors before they occur. These languages generally favor the LBYL style. Python, in contrast, is more likely to rely on its exception-handling mechanism when dealing with errors and exceptional situations.
With this brief introduction on strategies to deal with errors and exceptions, you’re ready to dive deeper into Python’s LBYL and EAFP coding styles and explore how to use them in your code.
The “Look Before You Leap” (LBYL) Style
LBYL, or look before you leap, is when you first check whether something will succeed and then only proceed if you know that it’ll work. The Python documentation defines this coding style as:
Look before you leap. This coding style explicitly tests for pre-conditions before making calls or lookups. This style contrasts with the EAFP approach and is characterized by the presence of many
if
statements. (Source)
To understand the essence of LBYL, you’ll use the classic example of handling missing keys in a dictionary.
Say that you have a dictionary containing some data, and you want to process the dictionary key by key. You know beforehand that the dictionary may include some specific keys. You also know that some keys may not be present. How can you deal with missing keys without getting a KeyError
that breaks your code?
You’ll have a few ways to tackle this problem. First, think of how you’d solve it using a conditional statement:
if "possible_key" in data_dict:
value = data_dict["possible_key"]
else:
# Handle missing keys here...
In this example, you first check if "possible_key"
is present in the target dictionary, data_dict
. If that’s the case, then you access the key and assign its content to value
. This way, you prevent a KeyError
exception, and your code doesn’t break. If "possible_key"
isn’t present, then you handle the issue in the else
clause.
This way to tackle the problem is known as LBYL because it relies on checking the precondition before performing the desired action. LBYL is a traditional programming style in which you make sure that a piece of code will work before you run it. If you stick to this style, then you’ll end up with many if
statements throughout your code.
This practice isn’t the only or the most common way to approach the missing-key issue in Python. You can also use the EAFP coding style, which you’ll check out next.
The “Easier to Ask Forgiveness Than Permission” (EAFP) Style
Grace Murray Hopper, a pioneering American computer scientist who made several outstanding contributions to computer programming, provided a valuable piece of advice and wisdom when she said:
It’s easier to ask forgiveness than it is to get permission. (Source)
EAFP, or easier to ask forgiveness than permission, is a concrete expression of this advice applied to programming. It suggests that right away, you should do what you expect to work. If it doesn’t work and an exception happens, then just catch the exception and handle it appropriately.
According to Python’s official glossary of terms, the EAFP coding style has the following definition:
Easier to ask for forgiveness than permission. This common Python coding style assumes the existence of valid keys or attributes and catches exceptions if the assumption proves false. This clean and fast style is characterized by the presence of many
try
andexcept
statements. The technique contrasts with the LBYL style common to many other languages such as C. (Source)
In Python, the EAFP coding style is pretty popular and common. It’s sometimes recommended over the LBYL style.
This popularity has at least two motivating factors:
- Exception handling is fast and efficient in Python.
- The necessary checks for potential problems are typically part of the language itself.
As the official definition says, the EAFP coding style is characterized by using try
… except
statements to catch and handle errors and exceptional situations that may occur during the execution of your code.
Here’s how to rewrite the example about handling missing keys from the previous section using the EAFP style:
try:
value = data_dict["possible_key"]
except KeyError:
# Handle missing keys here...
In this variation, you don’t check if the key is present before using it. Instead, you go ahead and try to access the desired key. If, for some reason, the key isn’t present, then you just catch the KeyError
in the except
clause and handle it appropriately.
This style contrasts with the LBYL style. Instead of checking preconditions all the time, it runs the desired action right away and expects the operation to be successful.
The Pythonic Way to Go: LBYL or EAFP?
Is Python better suited for EAFP or for LBYL? Which of these styles is more Pythonic? Well, it seems that Python developers generally tend to favor EAFP over LBYL. This behavior is based on a few reasons, which you’ll explore in a moment.
However, the fact remains that Python as a language doesn’t have an explicit preference regarding these two coding styles. Guido van Rossum, the creator of Python, has said as much:
[…] I disagree with the position that EAFP is better than LBYL, or “generally recommended” by Python. (Source)
As with many other things in life, the answer to the initial questions is: it depends! If the problem at hand suggests that EAFP is the best approach, then go for it. On the other hand, if the best solution implies using LBYL, then use it without thinking that you’re violating a Pythonic rule.
In other words, you should be open to using either LBYL or EAFP in your code. Either style can be the right solution depending on your specific problem.
Something that can help you decide which style to use is to answer the question: What is more convenient in this situation, preventing errors from happening or handling them after they happen? Think of the answer and make your choice. In the following section, you’ll explore the pros and cons of LBYL and EAFP, which can help you out with this decision.
The LBYL and EAFP Coding Styles in Python
To dive even deeper into when to use Python’s LBYL or EAFP coding style, you’ll compare both styles using a few relevant comparison criteria:
- Number of checks
- Readability and clarity
- Race condition risk
- Code performance
In the following subsections, you’ll use the criteria above to discover how the LBYL and EAFP coding styles can affect your code and which style would be appropriate according to your specific use case.
Avoiding Unnecessary Check Repetition
One of the advantages of EAFP over LBYL is that the former typically helps you avoid unnecessary check repetition. For example, say that you need a function that takes positive numbers as strings and converts them to integer values. You can write this function using LBYL, like in the example below:
>>> def to_integer(value):
... if value.isdigit():
... return int(value)
... return None
...
>>> to_integer("42")
42
>>> to_integer("one") is None
True
In this function, you first check if value
contains something that you can convert into a number. To do the check, you use the .isdigit()
method from the built-in str class. This method returns True
if all the characters in the input string are digits. Otherwise, it returns False
. Cool! This functionality sounds like the right way to go.
If you try out the function, then you conclude that it works as you planned. It returns an integer number if the input contains digits and None
if the input contains at least one character that isn’t a digit. However, there’s some hidden repetition in this function. Can you spot it? The call to int()
internally performs all the required checks to convert the input string to an actual integer number.
Because the checks are already part of int()
, testing the input string with .isdigit()
duplicates the checks already in place. To avoid this unnecessary repetition and its corresponding overhead, you can use the EAFP style and do something like this:
>>> def to_integer(value):
... try:
... return int(value)
... except ValueError:
... return None
...
>>> to_integer("42")
42
>>> to_integer("one") is None
True
This implementation completely removes the hidden repetition that you saw before. It also has other advantages that you’ll explore later in this tutorial, such as improving readability and performance.
Improving Readability and Clarity
To discover how using LBYL or EAFP affects the readability and clarity of your code, say that you need a function that divides two numbers. The function must be able to detect if its second argument, the denominator, is equal to 0
in order to avoid a ZeroDivisionError
exception. If the denominator is 0
, then the function will return a default value, which can be provided in the call as an optional argument.
Here’s an implementation of this function using the LBYL coding style:
>>> def divide(a, b, default=None):
... if b == 0: # Exceptional situation
... print("zero division detected") # Error handling
... return default
... return a / b # Most common situation
...
>>> divide(8, 2)
4.0
>>> divide(8, 0)
zero division detected
>>> divide(8, 0, default=0)
zero division detected
0
The divide()
function uses an if
statement to check if the denominator in the division is equal to 0
. If that’s the case, then the function prints a message to the screen and returns the value stored in default
, with is originally set to None
. Otherwise, the function divides both numbers and returns the result.
The problem with the above implementation of divide()
is that it puts the exceptional situation front and center, affecting the code’s readability and making the function unclear and difficult to understand.
In the end, this function is about computing the division of two numbers rather than about making sure that the denominator is not 0
. So, in this case, the LBYL style can generate a distraction, drawing the developer’s attention to the exceptional situation rather than to the mainstream case.
Now consider how this function would look if you wrote it using the EAFP coding style:
>>> def divide(a, b, default=None):
... try:
... return a / b # Most common situation
... except ZeroDivisionError: # Exceptional situation
... print("zero division detected") # Error handling
... return default
...
>>> divide(8, 2)
4.0
>>> divide(8, 0)
zero division detected
>>> divide(8, 0, default=0)
zero division detected
0
In this new implementation of divide()
, the function’s main computation is front and center in the try
clause, while the exceptional situation is caught and handled in the except
clause in the background.
When you start reading this implementation, you’ll immediately notice that the function is about computing the division of two numbers. You’ll also realize that in exceptional cases, the second argument can be equal to 0
, generating a ZeroDivisionError
exception, which is gracefully handled in the except
code block.
Avoiding Race Conditions
A race condition occurs when different programs, processes, or threads access a given computational resource at the same time. In this case, the programs, processes, or threads are in a race to access the desired resource.
Another situation in which race conditions appear is when a given set of instructions is processed in an incorrect order. Race conditions can cause unpredictable problems in the underlying system. They’re typically hard to detect and debug.
Python’s glossary page directly mentions that the LBYL coding style introduces the risk of race conditions:
In a multi-threaded environment, the LBYL approach can risk introducing a race condition between “the looking” and “the leaping”. For example, the code,
if key in mapping: return mapping[key]
can fail if another thread removeskey
frommapping
after the test, but before the lookup. This issue can be solved with locks or by using the EAFP approach. (Source)
The risk of race conditions not only applies to multi-threaded environments but also to other common situations in Python programming.
For example, say you’ve set a connection to a database that you’re working with. Now, to prevent issues that can corrupt the database, you need to check if the connection is active:
connection = create_connection(db, host, user, password)
# Later in your code...
if connection.is_active():
# Update your database here...
connection.commit()
else:
# Handle the connection error here...
If the database host becomes unavailable between the call to .is_active()
and the execution of the if
code block, then your code will fail by running into an error because the host isn’t available.
To prevent the risk of a failure like this, you can use the EAFP coding style and do something like this:
connection = create_connection(db, host, user, password)
# Later in your code...
try:
# Update your database here...
connection.commit()
except ConnectionError:
# Handle the connection error here...
This code goes ahead and tries to update the database without checking if the connection is active, which removes the risk of running into a race condition between the check and the actual operation. If a ConnectionError
occurs, then the except
code block handles the error appropriately. This approach leads to more robust and reliable code, saving you from hard-to-debug race conditions.
Improving Your Code’s Performance
Performance is an important concern when it comes to using LBYL or EAFP. If you come from a programming language with an expensive exception-handling process, then that concern is completely understandable.
However, most Python implementations have worked hard to make exception handling a cheap operation. So, you shouldn’t be worried about the cost of exceptions when you’re writing Python code. Exceptions can be faster than conditional statements in many cases.
As a rule of thumb, if your code deals with many errors and exceptional situations, then LBYL can be more performant because checking many conditions is less costly than handling many exceptions.
In contrast, if your code faces only a few errors, then EAFP is probably the most efficient strategy. In these cases, EAFP will be faster than LBYL because you won’t be handling many exceptions. You’ll just be performing the required operation without the extra overhead of checking preconditions all the time.
As an example of how using either LBYL or EAFP can impact your code’s performance, say that you need to create a function that measures the frequency of characters in a given text. So, you end up writing the following:
>>> def char_frequency_lbyl(text):
... counter = {}
... for char in text:
... if char in counter:
... counter[char] += 1
... else:
... counter[char] = 1
... return counter
...
This function takes a piece of text as an argument and returns a dictionary with the characters as keys. Each corresponding value represents the number of times that character appears in the text.
Note: In the Improving Your Code’s Performance section of this tutorial, you’ll find an example that complements the one in this section.
To build this dictionary, the for
loop iterates over each character in the input text. In each iteration, the conditional statement checks if the current character is already in the counter
dictionary. If that’s the case, then the if
code block increments the character’s count by 1
.
On the other hand, if the character isn’t in counter
yet, then the else
code block adds the character as a key and sets its count or frequency to an initial value of 1
. Finally, the function returns the counter
dictionary.
If you call your function with some sample text, then you get the result shown below:
>>> sample_text = """
... Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime
... mollitia, molestiae quas vel sint commodi repudiandae consequuntur
... voluptatum laborum numquam blanditiis harum quisquam eius sed odit
... fugiat iusto fuga praesentium optio, eaque rerum! Provident similique
... accusantium nemo autem. Veritatis obcaecati tenetur iure eius earum
... ut molestias architecto voluptate aliquam nihil, eveniet aliquid
... culpa officia aut! Impedit sit sunt quaerat, odit, tenetur error,
... harum nesciunt ipsum debitis quas aliquid.
... """
>>> char_frequency_lbyl(sample_text)
{'\n': 9, 'L': 1, 'o': 24, 'r': 22, ..., 'V': 1, 'I': 1}
The call to char_frequency_lbyl()
with sample_text
as an argument returns a dictionary containing character-count pairs.
If you think a little bit about the problem of finding the frequency of characters in a text, then you’ll realize that you have a finite number of characters to account for. Now think of how this fact can affect your solution. Having a finite number of characters means that you’ll be doing a lot of unnecessary checks to see if the current character is already in the counter.
Note: Python has a specialized Counter
class in the collections
module that was designed to tackle the problem of counting objects. Check out Python’s Counter: The Pythonic Way to Count Objects for more details.
Once the function has processed some text, then it’s pretty probable that the target character is already in counter
when you perform the check. In the end, all those unnecessary checks will add some performance costs to your code. This fact is especially true if you’re working with large pieces of text.
How can you avoid this extra overhead in your code? That’s when EAFP comes in handy. Get back to your interactive session and write the following function:
>>> def char_frequency_eafp(text):
... counter = {}
... for char in text:
... try:
... counter[char] += 1
... except KeyError:
... counter[char] = 1
... return counter
...
>>> char_frequency_eafp(sample_text)
{'\n': 9, 'L': 1, 'o': 24, 'r': 22, ..., 'V': 1, 'I': 1}
This function does the same as char_frequency_lbyl()
in your previous example. However, this time, the function uses the EAFP coding style instead.
Now you can run a quick timeit
performance test on both functions to get an idea of which one is faster:
>>> import timeit
>>> sample_text *= 100
>>> eafp_time = min(
... timeit.repeat(
... stmt="char_frequency_eafp(sample_text)",
... number=1000,
... repeat=5,
... globals=globals(),
... )
... )
>>> lbyl_time = min(
... timeit.repeat(
... stmt="char_frequency_lbyl(sample_text)",
... number=1000,
... repeat=5,
... globals=globals(),
... )
... )
>>> print(f"LBYL is {lbyl_time / eafp_time:.3f} times slower than EAFP")
LBYL is 1.211 times slower than EAFP
In this example, the performance difference between the functions is tiny. You can probably say that both functions perform the same. However, as the text size grows, the performance difference between the functions grows proportionally, and the EAFP implementation ends up being slightly more efficient than the LBYL implementation.
The takeaway from this performance test is that you need to consider which kind of problem you’re dealing with beforehand.
Is your input data mostly correct or valid? Are you dealing with a few errors only? Are your preconditions costly in terms of time? If your answer to these questions is yes, then lean towards EAFP. In contrast, if your data is in bad shape, you expect many errors to happen, and your preconditions are light, then favor LBYL.
Wrapping Up: LBYL vs EAFP
Wow! You’ve learned a lot about Python’s LBYL and EAFP coding styles. Now you know what these styles are and what their trade-offs are. To wrap up the main topics and takeaways of this section, check out the following table:
Criteria | LBYL | EAFP |
---|---|---|
Number of checks | Repeats checks typically provided by Python | Runs the checks provided by Python only once |
Readability and clarity | Has poor readability and clarity because exceptional situations seem to be more important than the target operation itself | Has enhanced readability because the target operation is front and center, while the exceptional situations are relegated to the background |
Race condition risk | Implies a risk of race conditions between the check and the target operation | Prevents the risk of race conditions because the operation runs without doing any checks |
Code performance | Has poor performance when the checks almost always succeed, and better performance when the checks almost always fail | Has better performance when the checks almost always succeed, and poor performance when the checks almost always fail |
Now that you’ve done a deep dive into comparing LBYL vs EAFP, it’s time to learn about a few common gotchas of both coding styles and how to prevent them in your code. Along with the topics summarized in the table above, these gotchas can help you decide which style to use in a given situation.
Common Gotchas With LBYL and EAFP
When you’re writing your code using the LBYL style, you must be conscious that you can be omitting certain conditions that need to be checked. To clarify this point, get back to the example in which you were converting string values to integer numbers:
>>> value = "42"
>>> if value.isdigit():
... number = int(value)
... else:
... number = 0
...
>>> number
42
Apparently, the .isdigit()
check fulfills all your needs. However, what if you have to process a string representing a negative number? Would .isdigit()
work for you? Run the above example with a valid negative number as a string and check what happens:
>>> value = "-42"
>>> if value.isdigit():
... number = int(value)
... else:
... number = 0
...
>>> number
0
Now you get 0
instead of the expected -42
number. What just happened? Well, .isdigit()
only checks digits from 0
to 9
. It doesn’t check negative numbers. This behavior makes your check incomplete for your new requirements. You’re omitting the negative numbers while checking the preconditions.
You can also think of using .isnumeric()
, but this method doesn’t return True
with negative values either:
>>> value = "-42"
>>> if value.isnumeric():
... number = int(value)
... else:
... number = 0
...
>>> number
0
This check doesn’t fulfill your needs. You need to try something different. Now think of how you can prevent the risk of omitting necessary checks in this example. Yes, you can use the EAFP coding style:
>>> value = "-42"
>>> try:
... number = int(value)
... except ValueError:
... number = 0
...
>>> number
-42
Cool! Now your code works as expected. It converts positive and negative values. Why? Because all the conditions that you need for converting a string to an integer number are implicitly included by default in the call to int()
.
Up to this point, it looks like EAFP is the answer to all your problems. However, that’s not true all the time. This style also has its gotchas. In particular, you must not run code with side effects.
Consider the following example, which writes a greeting message to a text file:
moments = ["morning", "afternoon", "evening"]
index = 3
with open("hello.txt", mode="w", encoding="utf-8") as hello:
try:
hello.write("Good\n")
hello.write(f"{moments[index]}!")
except IndexError:
pass
In this example, you have a list of strings representing different moments during the day. Then you use the with
statement to open the hello.txt
file in writing mode, "w"
.
The try
code block includes two calls to .write()
. The first one writes the greeting’s initial part to the target file. The second call completes the greeting by retrieving a moment from the list and writing it to the file.
The except
statement catches any IndexError
during the second call to .write()
, which does an indexing operation to get the appropriate argument. If the index is out of range, like in the example, then you get an IndexError
, and the except
block silences the error. However, the first call to .write()
already wrote "Good\n"
to the hello.txt
file, which ends up in an undesired state.
This side effect can be difficult to roll back in some situations, so you’d better avoid doing things like this. To fix this issue, you can do something like this:
moments = ["morning", "afternoon", "evening"]
index = 3
with open("hello.txt", mode="w", encoding="utf-8") as hello:
try:
moment = f"{moments[index]}!"
except IndexError:
pass
else:
hello.write("Good\n")
hello.write(moment)
This time, the try
code block only runs the indexing, which is the operation that can raise an IndexError
. If such an error occurs, then you just ignore it in the except
code block, leaving the file empty. If the indexing succeeds, then you write the complete greeting to the file in the else
code block.
Go ahead and try out both code snippets with index = 0
and index = 3
to check what happens with your hello.txt
file.
The second gotcha of EAFP appears when you use a broad exception class in your except
statement. For example, if you’re working with a piece of code that can raise several exception types, then you may think of using the Exception
class in the except
statement, or even worse, using no exception class at all.
Why is this practice a problem? Well, the Exception
class is the parent class of almost all of Python’s built-in exceptions. So, you’d be catching almost anything in your code. The conclusion is that you won’t have a clear idea of which error or exception you’re dealing with at a given moment.
In practice, avoid doing something like this:
try:
do_something()
except Exception:
pass
Apparently, your do_something()
function can raise many types of exceptions here. In all cases, you just silence the error and continue with your program’s execution. Silencing all the errors, including unknown ones, can cause unexpected bugs later, and it violates a fundamental principle in the Zen of Python: Errors should never pass silently.
To save yourself from future headaches, try to use concrete exceptions as much as possible. Use those exceptions that you consciously expect your code to raise. Remember that you can have several except
branches. For example, say that you’ve tested the do_something()
function, and you expect it to raise ValueError
and IndexError
exceptions. In this case, you can do something like this:
try:
do_something()
except ValueError:
# Handle the ValueError here...
except IndexError:
# Handle the IndexError here...
In this example, having multiple except
branches allows you to handle each expected exception appropriately. This construct also has the advantage of making your code easier to debug. Why? Because your code will immediately fail if do_something()
raises an unexpected exception. This way, you’ll prevent unknown errors from passing silently.
EAFP vs LBYL With Examples
Up to this point, you’ve learned what LBYL and EAFP are, how they work, and what the pros and cons of both coding styles are. In this section, you’ll dig a little more into when to use one style or the other. To do this, you’ll be coding a few practical examples.
Before starting the examples, here’s a summary of when to use LBYL or EAFP:
Use LBYL for | Use EAFP for |
---|---|
Operations that are likely to fail | Operations that are unlikely to fail |
Irrevocable operations, and operations that may have a side effect | Input and output (IO) operations, mainly hard drive and network operations |
Common exceptional conditions that can be quickly prevented beforehand | Database operations that can be rolled back quickly |
With this summary, you’re ready to start using LBYL and EAFP to code a few practical examples that’ll showcase the pros and cons of both coding styles in real-world programming.
Dealing With Too Many Errors or Exceptional Situations
If you expect your code to run into a ton of errors and exceptional situations, then consider using LBYL over EAFP. In this kind of situation, LBYL will be more secure and probably have better performance.
For example, say that you want to code a function that computes the frequency of words in a piece of text. To do this, you’re planning to use a dictionary. The keys will hold the words, and the values will store their counts or frequencies.
Because natural languages have too many possible words to consider, your code will be dealing with many KeyError
exceptions. Despite this fact, you decide to use the EAFP coding style. You end up with the following function:
>>> def word_frequency_eafp(text):
... counter = {}
... for word in text.split():
... try:
... counter[word] += 1
... except KeyError:
... counter[word] = 1
... return counter
...
>>> sample_text = """
... Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime
... mollitia, molestiae quas vel sint commodi repudiandae consequuntur
... voluptatum laborum numquam blanditiis harum quisquam eius sed odit
... fugiat iusto fuga praesentium optio, eaque rerum! Provident similique
... accusantium nemo autem. Veritatis obcaecati tenetur iure eius earum
... ut molestias architecto voluptate aliquam nihil, eveniet aliquid
... culpa officia aut! Impedit sit sunt quaerat, odit, tenetur error,
... harum nesciunt ipsum debitis quas aliquid.
... """
>>> word_frequency_eafp(sample_text)
{'Lorem': 1, 'ipsum': 2, 'dolor': 1, ..., 'aliquid.': 1}
This function creates a counter
dictionary to store the words and their counts. The for
loop iterates over the words in the input text. Inside the try
block, you try to update the current word’s count by adding 1
to its previous value. If the target word doesn’t exist as a key in counter
, then this operation raises a KeyError
.
The except
statement catches the KeyError
exception and initializes the missing key—a word—in counter
with a value of 1
.
When you call your function with some sample text, you get a dictionary with the words as keys and the counts as values. That’s it! You solved the problem!
Your function looks nice! You’re using the EAFP style, and it works. However, this function can be slower than its LBYL equivalent:
>>> def word_frequency_lbyl(text):
... counter = {}
... for word in text.split():
... if word in counter:
... counter[word] += 1
... else:
... counter[word] = 1
... return counter
...
>>> word_frequency_lbyl(sample_text)
{'Lorem': 1, 'ipsum': 2, 'dolor': 1, ..., 'aliquid.': 1}
In this variation, you use a conditional statement to check beforehand if the current word already exists in the counter
dictionary. If that’s the case, then you increment the count by 1
. Otherwise, you create the corresponding key and initialize its value to 1
. When you run the function with the sample text, you get the same dictionary of word-count pairs.
This LBYL-based implementation gets the same result as the EAFP-based implementation. However, it can have better performance. To confirm this possibility, go ahead and run the following performance tests:
>>> import timeit
>>> lbyl_time = min(
... timeit.repeat(
... stmt="word_frequency_lbyl(sample_text)",
... number=1000,
... repeat=5,
... globals=globals(),
... )
... )
>>> eafp_time = min(
... timeit.repeat(
... stmt="word_frequency_eafp(sample_text)",
... number=1000,
... repeat=5,
... globals=globals(),
... )
... )
>>> print(f"EAFP is {eafp_time / lbyl_time:.3f} times slower than LBYL")
EAFP is 2.117 times slower than LBYL
EAFP isn’t always the best solution to all your problems. In this example, EAFP is more than two times slower than LBYL.
So, if errors and exceptional situations are common in your code, then favor LBYL over EAFP. Many conditional statements can be faster than many exceptions because checking a condition is still less costly than handling an exception in Python.
Checking for Objects’ Type and Attributes
Checking for an object’s type is widely considered an anti-pattern in Python, and it should be avoided as much as possible. Some Python core developers explicitly called this practice an anti-pattern when they said the following:
[…] it is currently a common anti-pattern for Python code to inspect the types of received arguments, in order to decide what to do with the objects.
[This coding pattern is] “brittle and closed to extension” (Source).
Using the type checking anti-pattern will affect at least two core principles of Python coding:
- Polymorphism, which is when a single interface can process objects of different classes
- Duck typing, which is when an object has characteristics that determine whether it can be used for a given purpose
Python typically relies on an object’s behavior rather than on its type. For example, you should have a function that expects an argument with an .append()
method. In contrast, you shouldn’t have a function that expects a list
argument. Why? Because tying the function’s behavior to the argument’s type sacrifices duck typing.
Consider the following function:
def add_users(username, users):
if isinstance(users, list):
users.append(username)
This function works fine. It takes a username and a list of users and adds the new username to the end of the list. However, this function doesn’t take advantage of duck typing, because it relies on its argument’s type rather than on the required behavior, which is having an .append()
method.
For example, if you decide to use a collections.deque()
object to store your users
list, then you’ll have to modify this function if you want your code to continue working.
To avoid sacrificing duck typing with type checking, you can use the EAFP coding style:
def add_user(username, users):
try:
users.append(username)
except AttributeError:
pass
This implementation of add_user()
relies not on the type of users
but on its .append()
behavior. With this new implementation, you can start using a deque
object to store your list of users right away, or you can continue using a list
object. You won’t need to modify the function to keep your code working.
Python typically interacts with objects by directly calling their methods and accessing their attributes without checking the object’s type beforehand. The EAFP coding style is the right way to go in these cases.
A practice that also affects polymorphism and duck typing is when you check whether an object has certain attributes before accessing it in your code. Consider the following example:
def get_user_roles(user):
if hasattr(user, "roles"):
return user.roles
return None
In this example, get_user_roles()
uses the LBYL coding style to check if the user
object has a .roles
attribute. If that’s the case, then the function returns the content of .roles
. Otherwise, the function returns None
.
Instead of checking if user
has a .roles
attribute by using the built-in hasattr()
function, you should just go ahead and access the attribute directly with the EAFP style:
def get_user_roles(user):
try:
return user.roles
except AttributeError:
return None
This variation of get_user_roles()
is more explicit, direct, and straightforward. It’s more Pythonic than the LBYL-based variation. Finally, it can also be more efficient because it’s not constantly checking the precondition by calling hasattr()
.
Working With Files and Directories
Managing files and directories on your file system is sometimes a requirement in your Python applications and projects. When it comes to processing files and directories, multiple things can go wrong.
For example, say that you need to open a given file in your file system. If you use the LBYL coding style, then you can end up with a piece of code like this:
from pathlib import Path
file_path = Path("/path/to/file.txt")
if file_path.exists():
with file_path.open() as file:
print(file.read())
else:
print("file not found")
If you run this code targeting a file in your file system, then you’ll get the file’s content printed on your screen. So, this code works. However, it has a hidden issue. If, for some reason, your file gets deleted between the moment in which you check if the file exits and the moment in which you attempt to open it, then the file opening operation will fail with an error, and your code will crash.
How can you avoid this sort of race condition? Well, you can use the EAFP coding style, like in the code below:
from pathlib import Path
file_path = Path("/path/to/file.txt")
try:
with file_path.open() as file:
print(file.read())
except IOError as e:
print("file not found")
Instead of checking if you can open the file, you’re just trying to open it. If this works, then great! If it doesn’t work, then you catch the error and handle it appropriately. Note that you’re not taking the risk of running into a race condition anymore. You’re now safe.
Conclusion
Now you know that Python has the look before you leap (LBYL) and easier to ask forgiveness than permission (EAFP) coding styles, which are general strategies to deal with errors and exceptional situations in your code. You’ve also learned what these coding styles are and how to use them in your code.
In this tutorial, you’ve learned:
- The basics of Python’s LBYL and EAFP coding styles
- The pros and cons of LBYL vs EAFP in Python
- The keys to deciding when to use either LBYL or EAFP
With this knowledge about Python’s LBYL and EAFP coding styles, you’re now able to decide which strategy to use when you’re dealing with errors and exceptional situations in your code.
Free Bonus: 5 Thoughts On Python Mastery, a free course for Python developers that shows you the roadmap and the mindset you’ll need to take your Python skills to the next level.