Empty anvil.tables.SearchIterator should be "False"

I some code like this:

rows = app_tables.qsessions.search(...)
if rows:
    <do something if more than 0 rows exist>  

However this fails because an empty iterator evaluates to True. To me this seems counterintuitive as an empty list (or similar) evaluates to False.

Of course this can be fixed by converting to a list but I might not need the rows. And it increases the cognitive burden.

5 Likes

You can efficiently get the length of a search iterator with len():

if len(rows):
  # ... do something...

Yes, thanks. But wouldn’t you agree that the truthiness of an empty iterator should be False?

2 Likes

How does that compare to your experience with other iterators in Python?

Here’s what I get with a regular iterator:

>>> i = iter([1,2,3])
>>> bool(i)
True
>>> i = iter([])
>>> bool(i)
True

This is one thing that makes an iterator different from a container. An empty container is “falsy”. Virtually all other “objects” are “truthy”.

1 Like

Hmmm, hadn’t realised that. Thanks.
Is this the case for every iterator (by specification)?

1 Like

As I understand it, by default, every Python object is “truthy”, except for a few, select cases:

  • None
  • False
  • Zero
  • Empty containers (including strings, tuples, sets, lists, dicts, …)
  • Objects of custom-built classes, if they were explicitly built to override that default

I’m not familiar with any such custom classes. I suspect they’re fairly rare.

Iterators are not among the exceptions, so they’re “truthy”.

Reference: Truth Value Testing

Edit: To be fair, it isn’t necessarily a bad idea. C++ has the concept of “empty ranges”, which test false when the range is exhausted. This method of stopping is handy in situations where Python’s exception-based approach (raise StopIteration when done) is too slow or bulky.

But there are situations where determining “emptiness” can be expensive. For example, an iterator backed by a network socket, and a buffer. If the buffer is empty, then the iterator might be “empty”; but that could change in a microsecond, as data arrives from across the network. In this case, Python’s iterator model, which is more general, looks to me like a better fit.

By now, there’s on the order of a billion lines of Python code that work under the above rules. Changing the rules now is probably not an option.

2 Likes

Thx for the background info.

Came back to this, as I’ve hit this too. Going off the above linked Truth Value testing, the following

if rows:
  # ... do something...

definitely is more Pythonic, compared to the suggested:

if len(rows):
  # ... do something...

version. I haven’t really seen the latter used, though some go with the more explicit

if len(rows) > 0:
  # ... do something...

version, which works here too, of course, just unnecessarily verbose.

Though maybe this departure from the Python list behaviour is somewhat expected, since the SearchIterator doesn’t seem to behave like an iterator either (more like a list in every other aspect), so I’m sure there are preconceptions and expectations that get broken :slight_smile:

Still, if it “walks like list and quacks like list”, maybe it should be truthy like a list? :thinking: But I can totally be missing something too.

I also hit this recently, and spent some time tracking down why the if statement was True for an empty search iterator. Since my mental model of a search iterator is a list of rows, having it be True when empty wasn’t what I’d expected.

And while the len workaround works fine, it’s just a disconnect between what I’d expect in Python of a list-like object.

2 Likes

I also just hit this one, so +1 for making that empty search iterator false-y!

It’s a very common pattern to do:

if some_search_result:
    # do the thing

In this case (for performance reasons) we adjusted a function to return a search iterator instead of a list. We used that return elsewhere in the above pattern, and so we unexpectedly created a bug in production.

Python encourages duck-typing, and search iterators ought to be as “list-like” as possible!

Writing an is_empty(obj) function is probably the best way to go at this point. At least, it won’t break thousands of existing Apps. Changing the behavior of search iterators, at this point, will.

I would question that assumption. Currently, a search iterator always returns a truthy value. Developers who know this will be using len(results) == 0, which won’t break by changing an empty search iterator to be a falsey value.

And developers who don’t know about it will be running into this same issue, and coming here to ask about why an empty search iterator evaluates to True.

For a developer who knows about the issue to write if results would be nonsensical, the equivalent of writing if True.

2 Likes

That’s fair. Code that assumes a search iterator will always evaluate as True will break, but there probably isn’t much code like that out there.

1 Like

Every change might break someones code. :slightly_smiling_face:


https://xkcd.com/1172/

6 Likes

We’ve hit this one a couple more times - we’re on a streak.

It sounds like we are agreeing there is not really a reason to support empty search iterators being “truthy”.

I am also willing to bet, based on some of the recent (hard to debug) bugs we’ve faced, that changing the behavior will FIX more land-mined bugs out there than it will create. It is so natural to write:

search_results = some_table.search(**kwargs):
if not search_results:
    # Do the thing

but right now this fails. So Anvil, think we could change this? :slight_smile:

You could wrap the iterator in len() since 0 is falsey, and any other number is not.

search_results = some_table.search(**kwargs):
if not len(search_results):
    # Do the thing

and the custom code they have for table iterators and len() is low time complexity.

The challenge is not in implementing, it’s in REMEMBERING to implement! (though I appreciate your suggestion)

Which is why we end up with these bugs that are hard to identify, until the side effects of the logic have played out.

SearchIterators are designed to be list-like, and this specific variation from list behavior is causing us issues - we tend to think of them as lists, and so it’s very easy to fall into the trap of evaluating the truthiness expecting False if it’s empty.

For me, the behaviour should match any other iterator in Python, just as it does right now.

I’ve often thought linters should include a rule that flags when you use an iterator in a boolean expression, but I’ve not found one.

5 Likes

I agree with @owen.campbell on this one. In Python, it’s easy to confuse an iterable (a list or db table) with its iterator, as the former is often implicitly converted to the latter.

I’d rather not blur the distinction any further.

edit: Nevermind about what I said before.

1 Like