My Philosophy: EAFP vs. LBYL in Python

In Python, there are two philosophies when it comes to accessing items in a dictionary. The first is EAFP (“Easier to Ask Forgiveness than Permission”), which says to access the key and handle a KeyError if one arises. The other is LBYL (“Look Before You Leap”), which says to check for a key’s existence in the dictionary before trying to access it. As with many programming philosophies, different people have different opinions about which approach to use.

This post on StackOverflow that first brought these philosophies to my attention pointed me to the Python glossary entry on EAFP. The Python glossary seems to prefer EAFP in its definition of the term.

In the past, though, I’ve tended to prefer LBYL. The author of Effective Python: 90 Specific Ways to Write Better Python seems also to lean that way in his recommendation, “Prefer get Over in and KeyError to Handle Missing Dictionary Keys.”

I decided on a compromise between the approaches based on a sidebar at RealPython.

My philosophy is to use EAFP when we expect the key to be in the dictionary in most cases but can handle it if the key is absent, and to use LBYL when we don’t expect the key to be in the dictionary in most cases but want to do something if it is present. A recent situation illustrates the philosophy in action.

I have a function that accepts a dictionary as a parameter and uses it to perform some action, e.g.:

def my_function(my_data: Mapping[str, Any]) -> None:
    write_to_table(my_data['id'], my_data['name'])

This is an extremely simplified example of a function that uses the dictionary, my_data, to provide arguments to another function.

I want to add some optional data to the dictionary. When present, the function should take additional action based on that data. Most of the time, though, this optional data will not be in the dictionary. This is where I would use the LBYL approach because:

  • The key is optional
  • The normal case is for the key to be absent

So, I end up with the following:

def my_function(my_data: Mapping[str, Any]) -> None:
    write_to_table(my_data['id'], my_data['name'])
    other_data = my_data.get('other')
    if other_data:
        write_to_other_table(other_data)

I have another function that gets some data from a database and cleans it up a little before returning it to the caller, e.g.:

def read_from_table(my_id: int) -> Mapping[str, Any]:
    my_data = get_from_db(my_id)
    try:
        del my_data['id']
    except:
        pass
    return my_data

In this function, there is a key I want to delete from the dictionary. In this case, I expect the key to be present most (actually, all) of the time, but I can keep going even if the key is absent (since I want to delete the key anyway). I use the EAFP approach here because:

  • The key is optional
  • The normal case is for the key to be present

In summary, when it comes to choosing between EAFP or LBYL, my philosophy is to use the approach that best expresses my expectation for what is the normal case, i.e. what I expect to be true most often:

  • Use EAFP when you expect the key to be present most of the time
  • Use LBYL when you expect the key to be absent most of the time

I believe this makes it explicit to a reader what I expect the usual case to be, and being explicit in my intent is Pythonic.

My Philosophy: EAFP vs. LBYL in Python

Error-handling in Go

One of the tiresome aspects of Go is its standard idiom for error-handling. In that idiom, functions return errors, callers check those errors, and then take action accordingly. Typically, that action is for the caller to return the error to its own caller, gradually passing it up the call-stack for something else to handle. In practice, this results in a lot of boilerplate code that looks like this:

func MyFunc() (int, error) {
  n, err := someOtherFunc()
  if err != nil {
    return nil, err
  }

  if err := anotherFunc(n); if err != nil {
    return nil, err
  }
 
  return n, nil
}

The code above illustrates a couple of variations on the technique, but the pattern should be evident. Call after call, you check the err to see if it’s nil.

This has two implications. First, there’s a lot of redundancy; it can quickly become tedious typing the same code again and again. Second, it gets hard to see what’s going on with the code because it’s hidden behind all of this error handling code. I’ve found there are a couple of things I can do.

If I strive for functional cohesion, I tend to end up with functions that have fewer lines of code because they focus on specific tasks. As a result, I end up having to write less of this boilerplate code per function simply because there’s less code per function generally.

For more complex functions, such as those with procedural cohesion, I can often use some sort of centralized error to make the procedure in the function more apparent by hiding the error-handling. Let me provide a couple of examples of this.

Central Error Handling in a Builder

In a current project, I have a Builder type similar to the GoF builder pattern. Something like this:

type Builder struct{
...
}

func (b Builder) AddObjA(a ObjA) error {
...
}

func (b Builder) AddObjB(b ObjB) error {
...
}

func (b Builder) constructPartA() (Product, error) {
...
}

func (b Builder) constructPartB(p Product) (Product, error) {
...
}

func (b Builder) finishConstruction(p Product) (Product, error) {
...
}

func (b Builder) GetFinishedProduct() (Product, error) {
  product, err := b.constructPartA()
  if err != nil {
    return nil, err
  }
  product, err = b.constructPartB(product)
  if err != nil {
    return nil, err
  }
  return b.finishConstruction(product)
}

In the actual code, the GetFinishedProduct function does a lot more, but the idea is the same: GetFinishedProduct calls several functions in a specific sequence to build the final product from parts that were added to the builder earlier on. The sequence of GetFinishedProduct was obscured by all the error-handling code.

To make it clearer, I added a central error field to the Builder, and then adjusted my “construction” methods to check that error field before they did anything. If the error field was non-nil, the methods simply did nothing. As a result, my GetFinishedProduct could simply call the construction methods in sequence without worrying about their errors. It looks like this:

type Builder struct{
...
  err error
}

...

func (b Builder) constructPartA() Product {
  ...
  product, err := doSomeWork()
  if err != nil {
    b.err = err
    return nil
  }
  return product
}

func (b Builder) constructPartB(p Product) Product {
  if b.err != nil {
    return nil
  }
  ...
}

func (b Builder) finishConstruction(p Product) Product {
  if b.err != nil {
    return nil
  }
  ...
}

func (b Builder) GetFinishedProduct() (Product, error) {
  product := constructPartA()
  product = constructPartB(product)
  product = finishConstruction(product)
  return product, b.err
}

Error-handling is still going on, but it happens in construction functions that tend to be very focused, so they have few lines of code and thus less error-handling to do. Additionally, every construction function stores any of its errors in b.err; this puts the Builder into an “error” state so that future construction functions become no-ops. Each construction function (except the first) checks to see if the Builder is in an error state and simply skips any work if it is.

As a result of these changes, the GetFinishedProduct method becomes far simpler and its construction procedure is more apparent. It doesn’t need to do any error-handling itself since the construction functions are taking care of that. It simply returns whatever the construction functions produced along with the Builder’s error state.

Central Error Handling in a Function

I arrived at a similar approach recently using anonymous functions. I had a scenario where I was creating an implementation of the io.WriterTo interface, i.e. a function with the signature

func (o MyObject) WriteTo(w io.Writer) (int64, error) {...}

One of the challenges with this function was the need to count how many bytes I’d written when I was using other objects to do a lot of the writing and, depending on the methods called, I was getting back an int count of lines written in some cases and int64 in others. This, in addition to error handling, was making the code unwieldy.

What I arrived at was the use of some anonymous methods to help with the accumulation of total bytes written and managing the error-handling in a fashion similar to what I used in the Builder. The final product looked something like this:

func (o MyObject) WriteTo(w io.Writer) (int64, error) {
  total, err := int64(0), error(nil)

  print := func(s string) {
    if err != nil { return }
    var n int
    n, err = fmt.Fprint(w, s)
    total += int64(n)
  }
 
  write := func(wt io.WriterTo) {
    if err != nil { return }
    var n int64
    n, err = wt.WriteTo(w)
    total += n
  }

  // Actual procedure
  print(headerText)
  for _, child := range o.children { // children are io.WriterTo
    write(child)
  }
  print(footerText)

  return total, err
}

Here, the central error is a variable, err, that is accessible to the anonymous functions (assigned to the print and write variables) via closure. As in the Builder’s construction methods, these assign any errors to the err variable and they check to see if that variable is nil before doing any work, becoming “no-ops” if the error is non-nil.

This approach, again, allows the function to express its central procedure more clearly since it doesn’t have to handle errors after every function call.

The driver in this case wasn’t really the central error handling. The real driver was wanting a way to deal with the fact that fmt.Fprint returns the count of written bytes as an int while io.WriteTo returns it as an int64. This required having multiple variables to capture the intermediate counts before adding them to the total. That drove me toward the use of anonymous functions to wrap that work, and once they were in place, it was a simple step to centralizing the error handling.

Conclusion

While I wish that Go gave us the option to use structured exception handling (i.e. try…catch… blocks), there are ways to deal with its native approach without having to write a ton of redundant code in each function. Keep your functions functionally cohesive (i.e. focused on doing one thing) and they’ll have fewer lines of code, and hence, fewer calls that can result in errors. For functions with sequential or procedural cohesion, where the function’s work is more about calling other functions in sequence, look for opportunities to “centralize” your errors so that you can reduce the error-handling and make the procedure clearer.

Error-handling in Go

Go is not an object-oriented language

I’ve spent the better part of my career working with object-oriented languages. I was exposed to them first through Visual Basic. From there, I learned Java, then C#, and then C++. Along the way, I made a deliberate study of object-oriented design, drinking deeply from the design patterns promulgated by the Gang of Four, and learning to reshape everything I did in terms of object-oriented design. It’s the way I think about, decompose, and solve problems.

When I started learning Go (https://golang.org), I approached it from this same mindset, and at first it felt like I was making some traction. In time, though, I discovered it wasn’t working. I kept running up against brick walls where it felt like I was almost there, but the wall between what I was trying to accomplish and what Go could do was simply too high.

The problem wasn’t Go. The problem was that I was trying to treat Go like an object-oriented language when it isn’t. Some ideas that commonly appear in object-oriented languages, like interfaces, misled me. The sort of “pseudo-inheritance” that happens when you embed one structure into another made me feel like maybe I could treat the embedded structure as a base class and the embedding structure as a subclass. But while these things hinted at or “smelled” like object-orientation, they really weren’t.

I had to approach Go on its own terms. It is not an object-oriented language. Structures in Go aren’t not polymorphic the way that objects are in C# or C++. Embedding is not the same thing as subclassing. Structures are not classes.

I have a way to go still. 20+ years of an object-oriented habit is hard to break. But it is essential to succeed in using Go. As I approach Go on its own terms, I find myself less frustrated, and I start to see the patterns of use in Go that will help me achieve my goals. I start to see Go’s strengths more than its weaknesses.

My advice to anyone coming to Go from an object-oriented background like mine is to leave that thinking at the door lest you find yourself making things needlessly complicated and thoroughly frustrating. Approach Go with an open mind.

Go is not an object-oriented language