Why an assertion on an empty list/dict fails?

Viewed 565
assert {}

fails with AssertionError.

The issue led to this question was the following:

import xml.etree.ElementTree as ET

xml_element = ET.Element("tag")

assert xml_element

also fails with AssertionError.

assert evaluates the expression, I get that.

Also;

if []:
    print("foo")

won't print anything because empty list returns False but,

if [1]:
    print("foo")

would print foo.

My question is how could I have checked the internals of this behavior ?

2 Answers

When are things normally converted to true/false?

The question you are really asking is "what happens when you convert a list or dictionary to bool?". That's because if x: and assert x behave identically to if you wrote if bool(x): or assert bool(x). (By the way, putting that explicit call to bool() in those places is bad form, precisely because it's redundant.)

The answer is that lists and dictionaries (and strings and other containers) are True when converted to bool if they are not empty. Conversely, they're False when converted to bool if they are empty.

The terminology often used for this is that non-empty containers are "truthy" and empty containers are "falsey".

This has the interesting consequence that bool("False") evaluates to True - because "False" is a non-empty string!

Aside from containers, there are some other usual cases: numbers are truethy if they're non-zero (i.e. 27 and 0.5 are truthey while 0 and 0.0 are falsey), and the value None is falsey.

How to find out for other classes

how could I have checked the internals of this behavior ?

As said above, anything that is sufficiently like a collection is truethy when it's non-empty; the usual test for whether it's "like a container" is whether you can call len(x) successfully on it, in which case it is truethy whenever len(x) is non-zero.

Even if a class is not a container (or a number), it is still possible that it has some meaningful conversion to bool(). In fact you can even define this on your own classes by defining a __bool__() method. The only way to determine that, short of looking at the source code, is to look at the documentation for the library you're using.

If there is no such conversion defined by the library then bool(x) will just always return True (that's the default for classes with no __bool__() or __len__() methods defined). See Truth value testing in the official Python docs for a bit more information.

ElementTree specifically

xml.etree.ElementTree in particular, things are unfortunately a bit confusing. A looks a little bit like a container of elements: e[0] returns the first child element, and for child in e returns all the children, and len(e) returns the count of child elements (these are all documented in the official docs). It therefore makes perfect sense that bool(e) is true exactly when there is at least one child element - that fits with how all the rest of Python works. However, as Tomerikoo's answer shows in the source code, while this currently works, it's deprecated and may not work in future. The docs also mention this but it is extremely buried:

Caution: Elements with no subelements will test as False. This behavior will change in future versions. Use specific len(elem) or elem is None test instead.

The confusing logic behind this is explained in the code snippet below. The background is, maybe_child = e.find("child") finds a subelement of e with name "child", or None if no such element is found. But None is falsey, so it's tempting to check if this search succeeded by writing if maybe_child: - but that will actually count as False if a child was found but had no child elements itself. Here's the snippet from the docs:

element = root.find('foo')

if not element:  # careful!
    print("element not found, or element has no subelements")

if element is None:
    print("element not found")

From ElementTree's source code (of class Element):

class Element:
[...]
    def __init__(self, tag, attrib={}, **extra):
        if not isinstance(attrib, dict):
            raise TypeError("attrib must be dict, not %s" % (
                attrib.__class__.__name__,))
        self.tag = tag
        self.attrib = {**attrib, **extra}
        self._children = []
[...]
    def __bool__(self):
        warnings.warn(
            "The behavior of this method will change in future versions.  "
            "Use specific 'len(elem)' or 'elem is not None' test instead.",
            FutureWarning, stacklevel=2
            )
        return len(self._children) != 0 # emulate old behaviour, for now

Since you just initialized a new element, its _children is an empty list (as can be seen in the __init__), so if you understand why assert [] fails, you should understand why assert xml_element fails as-well.

Related