Two lines that surprise almost everybody the first time. Read what they print, then read what they say.
print(bool('False')) # -> True
print(bool('0')) # -> True
print(bool('')) # -> False
# bool() asks 'is there anything here?', not 'what does this say?'.
# Every non-empty string is truthy, including the ones that spell out no.
- Truthiness is about emptiness, not meaning
Python calls a string truthy when it has at least one character in it. It never reads the characters. So 'False', 'no', '0' and 'off' are all True, and the only falsy string is the empty one. bool() is the right tool for 'did I get anything?' and the wrong tool for 'what did I get?'.
- An allowlist says what you accept, out loud
Two sets of known spellings turn the question into a lookup. It is longer than a clever expression and it is self-documenting — a reader can see exactly which inputs are handled, and add one without re-deriving your logic.
- Normalise first, compare second
strip() and lower() collapse ' TRUE ', 'True' and 'true' into one value before anything is compared, so the sets stay small. Doing the tidying at the top and the deciding below is the same shape as parsing a date before doing arithmetic on it.
Do not try to compute the answer from the text. Decide in advance which spellings mean yes and which mean no, then look the input up. What should happen to the text before you look it up, so that ' TRUE ' and 'true' are the same?
Two sets — one of true spellings, one of false spellings. Strip and lowercase the input, then check membership. Anything in neither set is the error case.
Define the two sets at module level, in capitals — they are constants. In the function: cleaned = text.strip().lower(); return True if it is in the true set; return False if it is in the false set; otherwise raise ValueError with the original text in the message.
TRUE_WORDS = {'y', 'yes', 't', 'true', '1', 'on'} FALSE_WORDS = {'n', 'no', 'f', 'false', '0', 'off'} def parse_bool(text): cleaned = text.____().____() if cleaned in ____: return True if cleaned in ____: return False raise ____(f’cannot read {text!r} as a boolean')
TRUE_WORDS = {"y", "yes", "t", "true", "1", "on"}
FALSE_WORDS = {"n", "no", "f", "false", "0", "off"}
def parse_bool(text):
cleaned = text.strip().lower()
if cleaned in TRUE_WORDS:
return True
if cleaned in FALSE_WORDS:
return False
raise ValueError(f"cannot read {text!r} as a boolean")
The tempting shortcut is `return cleaned in TRUE_WORDS` — one line, and everything unrecognised becomes False. Then somebody types 'ture' in a config file, the feature stays off, and nothing anywhere says why. A ValueError naming the value turns a week of confusion into a line in a log. Unknown input is not a no.
Two sets mean 'off' and 'zzz' are treated differently, which is the whole point. It also documents the accepted vocabulary in both directions, so the next person can add 'enabled' and 'disabled' without guessing what the function currently tolerates.
f'{text!r}' shows quotes and any invisible characters, so a value that turns out to be 'true\n' or ' true' with a non-breaking space is diagnosable at a glance. The plain form would print them identically to the ones that work.
Do not write this if a library already owns the format. os.environ handling is usually better served by a settings library — Pydantic, environs, django-environ — which parses types, validates and reports every problem at startup rather than at the moment the value is first read. configparser has getboolean() built in with almost this exact vocabulary. Write your own for a small internal format you control, or when you need the error message to say something a library cannot.
- Add an optional default so parse_bool(text, default=False) returns it for empty input only, while still refusing genuine nonsense — and decide whether that is an improvement or a way to reintroduce the bug.
- Accept a real bool or an int as input as well as text, so the function survives being called on already-parsed config.
- Compare your vocabulary with configparser.ConfigParser.BOOLEAN_STATES and decide whether matching an existing convention is worth more than covering more spellings.