None, Truthiness, and Initialization

Updated

September 8, 2026

None, Truthiness, and Initialization

None means “not set.” False, 0, and [] are set — they just happen to be empty or no. The boring default is to test for the value you mean, not for “looks false in an if.”

Mental model

Python has a small set of values that are falsy in a boolean context: None, False, 0, 0.0, "", [], {}, set(), and a few others. Everything else is truthy, including "0", "False", and [0].

That list is a convenience for if items: meaning “is there anything here?” It is a trap when 0 is a legal table number, when [] is a legal empty note list, or when None means “the cook has not answered yet.”

Initialization is how a function gets a starting container. A default argument is evaluated once, when the def line runs, not each call. A mutable default ([], {}) is shared across calls. That is the classic Python footgun.

Worked examples

Case 1: Four different “no”s

Save as four_nos.py. A missing close time, an unpaid check, a zero-cover walk-in, and an empty note list are not the same fact.

# four_nos.py
def describe(ticket):
    if ticket["closed_at"] is None:
        print("still open")
    if ticket["paid"] is False:
        print("not paid")
    if ticket["covers"] == 0:
        print("walk-in, covers unknown")
    if ticket["notes"] == []:
        print("no notes yet")


def main():
    ticket = {
        "closed_at": None,
        "paid": False,
        "covers": 0,
        "notes": [],
    }
    describe(ticket)
    print("bool(None)", bool(None))
    print("bool(False)", bool(False))
    print("bool(0)", bool(0))
    print("bool([])", bool([]))
    print("None == False", None == False)
    print("None == 0", None == 0)
    print("False == 0", False == 0)


if __name__ == "__main__":
    main()

Run:

uv run python four_nos.py

Output:

still open
not paid
walk-in, covers unknown
no notes yet
bool(None) False
bool(False) False
bool(0) False
bool([]) False
None == False False
None == 0 False
False == 0 True

All four are falsy. Only False == 0 is actually equal, because bool subclasses int. None equals neither. Test is None, is False, == 0, and == [] when those meanings matter.

Case 2: if notes: is fine for “any notes”

When empty really means “skip,” a truthiness check is the boring line.

# any_notes.py
def print_notes(notes):
    if notes:
        print("notes:", ", ".join(notes))
    else:
        print("no notes")


def main():
    print_notes(["window", "allergy"])
    print_notes([])


if __name__ == "__main__":
    main()

Run:

uv run python any_notes.py

Output:

notes: window, allergy
no notes

That is the intended use. Do not use if notes: when notes might be None (not loaded) versus [] (loaded, empty). Those two states need different handling.

Case 3: Default None, then make a list

When a function should start with an empty list unless the caller passes one, default to None and create the list inside.

# add_note.py
def add_note(note, notes=None):
    if notes is None:
        notes = []
    notes.append(note)
    return notes


def main():
    a = add_note("window")
    b = add_note("allergy")
    shared = []
    add_note("booth", shared)
    add_note("quiet", shared)
    print(a)
    print(b)
    print(shared)


if __name__ == "__main__":
    main()

Run:

uv run python add_note.py

Output:

['window']
['allergy']
['booth', 'quiet']

Each call that omits notes gets a new list. The caller who passes shared keeps one list on purpose.

The trap

A mutable default argument is evaluated once. Every call that omits the argument appends to the same list.

# sticky_notes.py
def add_note(note, notes=[]):
    notes.append(note)
    return notes


def main():
    first = add_note("window")
    second = add_note("allergy")
    print("first ", first)
    print("second", second)
    print("same object", first is second)


if __name__ == "__main__":
    main()

Run:

uv run python sticky_notes.py

Output:

first  ['window', 'allergy']
second ['window', 'allergy']
same object True

first and second are one list. The second ticket inherited the first ticket’s note. The fix is Case 3: default None, assign notes = [] inside the function.

The same trap hits def label(ticket, extra={}): — one dict, shared forever. Default to None for every mutable.

The boring rule

  • None means missing. Test with is None / is not None.
  • 0, [], "", and False are real values. Test them by name when 0 or empty is legal.
  • if items: is fine when you only care “any vs none.”
  • Never default an argument to [] or {}. Use None and create inside.
  • Do not write if not ticket["closed_at"] if closed_at could be None or 0 for different reasons.

Try this

  1. Change four_nos.py so covers is None (unknown) versus 0 (walk-in). Print two different sentences.
  2. In add_note.py, add a third call add_note("vip") and confirm you still get a fresh list.
  3. Copy sticky_notes.py and default notes=None instead. Re-run. first is second should be False.