Skip to content

LBYL vs. EAFP (isinstance vs. try/catch) #3738

Open
@michalhabera

Description

@michalhabera

Easier to Ask Forgiveness than Permission (EAFP) style recently added to some of the python interfaces, see e.g. https://github.com/FEniCS/dolfinx/blob/main/python/dolfinx/la/petsc.py#L130 leads to

  1. large performance overhead, see example below and,
  2. opaque error handling. If any of the deep nested try/except blocks fails then user sees the outer exception caught which looks like an error message, but is not an error. The logic in the code also depends on the type of exception caught thus relies on the code being called raising consistently.

The following benchmark

import time
import platform
import datetime
import textwrap
import numpy as np

N = 1_000_000


def eafp():
    total = 0
    for k in range(N):
        try:
            raise RuntimeError
        except RuntimeError:
            try:
                raise RuntimeError
            except RuntimeError:
                total += k
    return total


def lbyl():
    total = 0
    for k in range(N):
        if isinstance(k, ()):
            pass
        else:
            if isinstance(k, ()):
                pass
            else:
                total += k
    return total


def run_benchmark(fn, repeats=3):
    times = []
    for _ in range(repeats):
        start = time.perf_counter()
        fn()
        times.append(time.perf_counter() - start)

    times = np.array(times)
    mean = times.mean()
    std = times.std()
    return mean, std  # use best-of-repeats to reduce noise


lbyl_time_mean, lbyl_time_std = run_benchmark(lbyl)
eafp_time_mean, eafp_time_std = run_benchmark(eafp)


benchmark_info = textwrap.dedent(f"""
Python {platform.python_version()} on {platform.python_implementation()}
Date: {datetime.datetime.now().isoformat(sep=" ", timespec="seconds")}

Iterations            : {N:,}

RESULTS
------------------------
EAFP (try/except)      : mean {eafp_time_mean:.4f} s, std {eafp_time_std:.4f} s
LBYL (isinstance)      : mean {lbyl_time_mean:.4f} s, std {lbyl_time_std:.4f} s
Speed-up (LBYL/EAFP)   : mean {eafp_time_mean / lbyl_time_mean:.1f} × faster
""")

print(benchmark_info)

executes for me in dolfin/dolfinx:nightly on M4 as

Python 3.12.3 on CPython
Date: 2025-05-20 13:51:58

Iterations            : 1,000,000

RESULTS
------------------------
EAFP (try/except)      : mean 0.1267 s, std 0.0017 s
LBYL (isinstance)      : mean 0.0287 s, std 0.0001 s
Speed-up (LBYL/EAFP)   : mean 4.4 × faster

In my opinion EAFP should only be used when we expect a small number of misses (miss == exception being caught), and without double try/except clause.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions