PYTHON / VARIABLES AND DATA TYPES
Booleans and None
Use True, False and None correctly: know which values Python treats as falsy, why bool is an int, and when to test with is None.
What you will learn
- List the falsy builtins: False, None, 0, 0.0, '', (), [], {}, set()
- Test for absence with `is None` instead of `== True` or bare truthiness
- Predict that bool subclasses int, so sum([True, False, True]) is 2
- Control what `if obj:` means for your own classes via __bool__ or __len__
Understanding Booleans and None
The type bool has exactly two values, written True and False, and they are keywords rather than ordinary names, so you cannot rebind them. bool is a subclass of int: True behaves as 1 and False as 0 in arithmetic, indexing and sum(). That inheritance is why isinstance(True, int) is True, and it is what lets sum(flags) count how many conditions held without any conversion step.
Separately from the bool type, every Python object has a truth value, because any object can appear in an `if` or after `not`. Python asks the object itself: it calls __bool__ if defined, otherwise __len__ (empty means false), and if neither exists the object is true. So the falsy set is small and specific: False, None, numeric zeros, and empty containers and strings. The mental model for `if x:` is therefore "x is not empty and not zero and not missing", which is a much broader question than "x is True".
None is the sole instance of NoneType and means "there is no value here". A function that falls off its end, or one that mutates something in place like list.sort(), returns None, and None is also the conventional sentinel for an unset default argument. None is falsy, but it is not equal to False, 0, or '' — it is a distinct object. Because exactly one None exists per interpreter, `is None` is both the fastest and the most precise test, and it keeps working even for objects whose __eq__ is unusual.
flag = True
print(flag, type(flag))
print(True + True, False * 10)
for value in (0, 0.0, '', '0', [], [0], None):
print(repr(value).ljust(6), '->', bool(value))
def log(message):
print('LOG:', message)
result = log('saved')
print('returned:', result)
print('result is None:', result is None)
print('result == False:', result == False)A boolean context asks an object for its truth value, so "falsy" is much wider than False, and None is a separate value meaning "no value at all".
Worked examples
None as a sentinel, not as "falsy"
Shows why `x or default` corrupts settings whose real value is 0 or an empty string.
def setting(config, key, default):
value = config.get(key)
if value is None:
return default
return value
def setting_buggy(config, key, default):
return config.get(key) or default
config = {'retries': 0, 'prefix': ''}
for key in ('retries', 'prefix', 'missing'):
print(key, repr(setting(config, key, 'FALLBACK')), repr(setting_buggy(config, key, 'FALLBACK')))Example explained
Line 1dict.get returns None for a missing key, so `value is None` asks exactly one question: was the key absent?
Line 2`config.get(key) or default` asks a different question — is the value falsy — and 0 and '' are falsy, so both stored settings are thrown away.
Line 3The last row shows the two versions agreeing: when the key really is missing, both produce the fallback, which is why this bug hides in testing.
Defining truthiness for your own objects
Demonstrates the __bool__ then __len__ lookup order and the default-true rule.
class Basket:
def __init__(self, items):
self.items = items
def __len__(self):
return len(self.items)
class Switch:
def __init__(self, on):
self.on = on
def __bool__(self):
print('asked Switch for truth value')
return self.on
print(bool(Basket([])), bool(Basket(['apple'])))
if Switch(False):
print('on')
else:
print('off')
print(bool(object()))Example explained
Line 1Basket has no __bool__, so Python falls back to __len__: length 0 is falsy, any other length is truthy.
Line 2The printed line proves that `if Switch(False):` actually calls __bool__ — the condition is a method call, not a comparison.
Line 3A plain object() defines neither hook, so it is true; this is why `if some_object:` almost never tells you whether the object is 'valid'.
Booleans are integers
Uses True and False directly as numbers and as list indices.
temps = [12, 30, 18, 41, 5]
hot = [t > 25 for t in temps]
print(hot)
print(sum(hot), 'of', len(hot), 'readings above 25')
print(isinstance(True, int), True == 1, True + 1)
print(['cold', 'hot'][temps[0] > 25])Example explained
Line 1A comparison like t > 25 evaluates to a real bool object, so the comprehension builds a list of True/False values.
Line 2sum(hot) works because bool inherits from int: each True contributes 1, so the sum is a count.
Line 3temps[0] > 25 is False, which is usable as the index 0, selecting 'cold' — legal, but clearer written as an if/else expression.
Important notes
`and` and `or` return one of their operands, not a bool: `'' or 'x'` is 'x' and `0 and 5` is 0. Wrap the expression in bool() if you need an actual True/False.
True, False and None are keywords in Python 3, so `None = 5` is a SyntaxError; and because exactly one None object exists per interpreter, `is None` is guaranteed correct in a way that `is 0` or `is ''` is not.
Common mistakes
Writing `nums = nums.sort()`: sort() mutates the list in place and returns None, so nums becomes None and the next loop over it raises TypeError: 'NoneType' object is not iterable.
Filling in a default with `value or fallback`: a legitimate 0, '' or [] is falsy and gets silently replaced by the fallback, producing wrong data with no error.
Testing `if flag == True:` instead of `if flag:`: truthy values such as 'yes', 2 or [1] fail the equality test, so the branch is skipped even though the value clearly means "on".
Try it yourself
Change, predict, then run
Write average(values) that returns None for an empty list and the mean otherwise, then call it with [] and with [2, 4] and print a label chosen with `if result is None:` — check that switching the test to `if not result:` mislabels the input [0, 0].
Open the Python workspaceCheck your understanding
A search function returns the index of a match and simply falls off the end when there is no match. The caller writes `if not result: print('not found')`. What is the flaw?
- A match at index 0 is reported as 'not found', because both 0 and the returned None are falsy; the caller must test `result is None`
- Falling off the end returns False, so `not result` is always True and every search reports 'not found'
- `not` cannot be applied to an integer, so the check raises a TypeError whenever a match is found
- The function returns None only when the list is empty, so any non-empty list is handled correctly
Show answer
A function with no return statement returns None, which is falsy — and so is the perfectly valid index 0, so `not result` collapses two different outcomes into one branch; `result is None` distinguishes them. Option 2 is tempting because 'no result' feels like False, but Python returns None, not False, and a found index of 1 or 2 would still pass the check, so the failure is specific to index 0.