Add safe `.get` method to List

“marginally”, not “significantly”. And it’s likely to be harder to debug - with the second version, you can simply add a print(state) in the except clause to work out why you’re getting a default value you don’t expect. I do that all the time with exploratory data analysis (which is where terseness is often most tempting).

1 Like

Do you have a recent source that EAFP is the Pythonic way?

With the proliferation of type checking, LBYL is far more common now since isinstance checks allow type checkers to make deductions whereas try-except blocks don’t.

1 Like

Type checking is not replacing other systems in Python. A lot of Python code is not run through MyPy or any other type checker. (There isn’t even a type checker in the standard library.) So if you want to attest that LBYL is now the “Pythonic way”, you’ll need to show some evidence of this change.

1 Like

I didn’t say it was the Pythonic way. I just said that it’s more common than before (in my opinion).

2 Likes

Type checked code is not LBYL. In fact

def get_a(d: dict):
    return d.get("a")

is about as EAFP as you could possibly get - the input parameter to get_a isn’t checked at all at runtime to confirm it has a get method. There’s an optional lint check[1] that can be run on calling code to ensure it doesn’t pass data that would fail the missing validation, but it’s perfectly possible to call get_a(42) - and get an exception because the input isn’t valid.

So I’d say that actually, type hints encourage EAFP-style code. Unfortunately, they also encourage people to omit checks on the (mistaken) assumption that people won’t ever forget to run a type checker on the calling code.


  1. mypy ↩︎

1 Like

To me, this isn’t what EAFP means. To me, EAFP means using exceptions to do flow control. So, for example, it would be EAFP if you had:

def get_a(d: dict | list):
    try:
        return d.get("a")
    except AttributeError:
        return d[0]

The except clause is the “asking for forgiveness” part of EAFP.

Here’s an example of EAFP code in the real world. If this were annotated, n would be tuple[Node, dict] | Node. The TypeError is designed to split out the tuple case (since the tuple contains a dict, and dicts aren’t hashable). But a type checker can’t figure out this flow control, so this won’t type check properly.

If they wanted to write this using LBYL, they could change it to:

            if not isinstance(n, tuple):
                newnode = n not in self._node  # Type checkers know that n is a Node
                newdict = attr
                deduced_node = n
            else:
                deduced_node, ndict = n  # Type checkers can figure out that n is a tuple[Node, dict]
                newnode = deduced_node not in self._node
                newdict = attr.copy()
                newdict.update(ndict)

The type checker would then understand that deduced_node is a node, and understand what n is in each case.

1 Like

Personally, I don’t tend to use the term EAFP that much. I only used it in contrast to LBYL, which is (IMO) much more clearly defined as “check before you do something”. Whereas I personally find it more Pythonic to just do stuff, and handle problems elsewhere in an exception handler (the precise definition of “elsewhere” depends on context - it can be in a localised try…except, or a function level exception handler, or an application level error handling and reporting routine).

For me, EAFP is more about saying that “asking permission is a PITA, and we don’t need to as long as we can get forgiveness later, by tidying up the mess somehow”. What’s clearly wrong is doing neither - not checking for problems, and not being prepared to deal with them either. My main concern with type checking is that it encourages people to think that problems can’t happen because “the type checker will prevent them”. It won’t - all it will do is tell you there’s a problem if you run the checker. That’s useful information, but not a guarantee.

That’s a limitation of type checkers, and not a reason to change the code (although I admit people do change code for bad reasons, one of which is certainly “my tool can’t cope with this”).

2 Likes

Okay, we have different definitions then. I think EAFP means use exceptions to do flow control whereas LBYL is using conditional statements to do flow control: The exception handling being the asking for forgiveness part of EAFP and the conditional statements being the looking part of LBYL.

Fair enough.

I agree. Anything gives a sense of security sometimes gives a false sense of security.

I don’t think it’s realistic for type checkers to try to narrow types by the exceptions they expect to be raised since exceptions raised are not annotated. Even if they were, this wouldn’t work in general (when both sides of a condition could raise the same exception) and would be an extremely tricky to reason about.

It’s a matter of personal opinion, but I find the instance check to be significantly more readable. It’s not at all obvious that the tuple case is being separated by the fact that the tuple contains a dict, and a dict is not hashable, which makes dict.__contains__ raise a TypeError! That is incredibly convoluted logic compared with isinstance(n, tuple).

And in general, reasoning about types is integral to programming. Hungarian notation emerged because of this exact reason. So, relying on exceptions to do flow control would, in my opinion, necessitate comments to explain which types are along which branches. On the other hand, using instance checks makes the type check explicit rather than implicit, and obviates some comments about types.

1 Like

It’s not really about how you do flow control as what governs it. Consider:

if is_readable(fn):
    read_config(fn)
else:
    use_defaults()

vs

try:
    read_config(fn)
except FileNotReadable:
    use_defaults()

The LBYL approach frequently leads to TOCTOU problems, which can be incredibly hard to diagnose. EAFP keeps all of the logic in one place - somewhere inside read_config there needs to be handling for unreadable files anyway.

Do you have a recent source that EAFP is the Pythonic way?

Yes. See the message by @elis.byberi immediately above my first comment.

To be clear, I am not necessarily claiming that there are authorative sources stating that EAFP is “the Pythonic way” (or LBYL for that matter). No, I am simply bringing attention to the fact that this is a commonly expressed sentiment.

In this case, I don’t actually care how the situation gets resolved. I would be 100% fine with either list.get or except-expression (PEP 463). The problem is that both ideas are often dismissed with essentially conflicting justifications.

Let the pro-EAFP and pro-LBYL people fight it out, and then we’ll accept one of these proposals. :grin: (Obviously, it’s not quite that simple. These proposals might have their own pros and cons aside from their EAFP-iness, but at least we should be able to evaluate their actual merits instead of getting the knee-jerk responses of “no, you should just use EAFP/LBYL”.)

I am gonna pull my ESL card here. I meant to use “significantly” in the same way that it would be used in a scientific paper (i.e. the readability improvement is not insignificant/random/unrelated to the proposed syntax change). Perhaps, “definitely” or “clearly” would have been more appropriate.

This seems like a pretty weak argument to me. Nothing is stopping you from converting the expression back to its statement form while you are debugging, and then converting it back after you fix the issue. You could use essentially the same argument to forbid almost any non-trivial expression.

For example, you can’t do print(state) inside a list comprehension[1]. So by the same logic, list comprehensions are also hard to debug. And so are ternary x if cond else y expressions. And chained function calls. And…


  1. well, okay, you technically can replace any expression with something like (print(state), expression)[-1], but this also applies to the proposed except-expression syntax ↩︎

1 Like

Zen of Python:

Errors should never pass silently.
Unless explicitly silenced.

EAFP: This common Python coding style assumes the existence of valid keys or attributes and catches exceptions if the assumption proves false.

Hm. It sounds to me like the Zen of Python is talking about actual errors here rather than specifically using the exception mechanism for control flow. I am pretty sure that you can make the Zen of Python support almost any reasonable position through “careful interpretation”.

On the other hand, it does indeed seem that the glossary recommends EAFP over LBYL.

LBYL:
<snip> the LBYL approach can risk introducing a race condition <snip>. This issue can be solved with locks or by using the EAFP approach.

EAFP:
<snip> This clean and fast style is <snip>. The technique contrasts with the LBYL style common to many other languages such as C.

This does sound like a pretty unambiguous endorsement of EAFP as “Pythonic”.

I wonder if we can get a comment from @guido on this issue. Maybe his opinion has changed since his rejection of PEP-463 back in 2014?

1 Like

Its not a matter of use one not the other.
It is a matter of what is suitable code for a specific problem.
Not an absolute right vs wrong.

Checking that a key is in a dict is often clearer as LBYL.
But deleting an optional file has a race condition between os.path.exists and os.remove so EAFP is required for correctness.

Both are race conditions, but programmers like to assume that their code is single-threaded. There is no fundamental difference here.

Only if you are using threads and sharing the dict with the threads.
And yes i know there are threads hiding inside library code.

Yes. Like I said, programmers like to assume that their code is, and will forever be, single-threaded.

This is similar to the assumptions that your code will only ever run on CPython, only ever run on [whichever OS you use], etc, etc, etc. We make these assumptions ALL THE TIME. All of us do.

Any time we can be in the habit of writing code that doesn’t make these assumptions, this makes our code more resilient. For example, with open(...) is better than f = open(...).read() because it makes fewer assumptions about the garbage collection model. I say the same applies here - why open yourself up to threading problems when you have a safer option right there?

eXterme programming argues strongly for only coding to the requirements you have now.
Coding for a future unclear requirement is shown to be almost always wasted effort.

Now that is not to say that you should not use best practice, however, as always with engineering, use your best judgment and keep learning.

And that’s exactly what I’m saying: use best practice, such as EAFP, where there is no additional cost to being safe. Why use the unsafe method when the safe method is just as easy?

I tend to favour using special return values for expected failures and then localising the place where exceptions are caught as much as possible e.g.:

result = read_config(fn)
if result is None:
    result = default_config()

Of course somewhere inside read_config an exception probably is being caught but if it is an expected exception that I will want to handle then I would rather turn it into a return value at the earliest opportunity (with the smallest possible block of code inside the try block). Then I can avoid catching exceptions anywhere else so that unexpected exceptions are always guaranteed to bubble up. I would still consider this to be EAFP just without using exceptions for flow control everywhere.