Being negative with Python
Observations on the falsy behaviours of Python 3

Python is a dynamically typed programming language. This means it assigns types to variables whilst the program is running, as opposed to getting its types enforced via the programmer and/or a compiler. Whilst Python has added type hints to allow programmers to mark up their code with types, these act more like suggestions than rules when compared to alternative languages with strict type systems such as Rust. Thankfully, python does have a strong type system - forbidding operations between nonsensical types instead of silently validating or failing on them.
Let's explore some caveats with determining negatives in Python. We'll use a series of items we would typically associate with a negative state:
The
Falsevalue0An empty list
[]An empty string
''The
NonevalueAn empty dictionary
{}An empty tuple
()
bool typecast and the not negation
If we cast these to the bool type, we are able to confirm that Python can determine that all of these are False.

If we check these same test conditions with the not keyword, we get a similar result:

This is great. Python has a convenient way for us to simply determine if a normal data structure is Truthy or Falsy and all our problems are solved.
Explicit False check
You might be wondering if we could have been more explicit. Why didn't we just check if the value was False directly?

Here we have checked the items in two ways. The first, with ==, is a value-based equality to False, which provides two positive matches:
False == False0 == False
What this check is doing is more that just checking if these values are "falsy" - we need to know if they are False. That's right - "falsy" and False do not mean the same thing; False is stricter. In python 0 is considered equal to False in addition to also being "falsy".
The second check, with is, is a reference-based equality to False, which provides one positive match - False is False. The is operator checks if both sides point to the same object in memory, which is why we only match when we compare the same object to itself.
None for me thanks
What about if we just check if nothing was there at all:

It turns out that for both value-based equality and reference-based equality, only the None 'falsy' item was picked up. This is because most 'falsy' things still represent an actual "thing" - the thing just happens to be negative. None represents the absence of a thing entirely, which is why you can have Optional[bool] types to indicate that sometimes a boolean is provided or it's just an undefined nothingness.
The trickiness with Nones is that they often aren't just written in code but appear implicitly. A classic example will be from returning a function like so:

As you can see here, in both cases this empty_function() is caught by the if condition. This is because it returns nothing... which is None - it's just not written in the program explicitly.
In summary, the best solution for broad-spectrum negation is simply not. If you require more strict false checking, you will need to determine your use case as appropriate. You should also be mindful that None is sometimes implicit, and the differences between value-based equality with == and reference-based with is.

