You're searching for a user by email. If found, return the user object. If not found, what do you return? Not an empty string (that's a valid email). Not zero. None is Python's way of saying "no value here."

None vs empty values

None is different from empty string, zero, or empty list.

score_not_taken
none_vs_empty.py
Replay: real traced execution (multi-file project)
# None is different from "empty" or "zero" values
empty_string = ""
zero = 0
empty_list = []
nothing = None

print("=== Type Comparison ===")
print(f"type(''): {type(empty_string)}")
print(f"type(0): {type(zero)}")
print(f"type([]): {type(empty_list)}")
print(f"type(None): {type(nothing)}")

print("\n=== Truthiness ===")
print(f"bool(''): {bool(empty_string)}")    # False (falsy)
print(f"bool(0): {bool(zero)}")              # False (falsy)
print(f"bool([]): {bool(empty_list)}")       # False (falsy)
print(f"bool(None): {bool(nothing)}")        # False (falsy)

print("\n=== Identity ===")
print(f"'' is None: {empty_string is None}")   # False
print(f"0 is None: {zero is None}")             # False
print(f"[] is None: {empty_list is None}")      # False
print(f"None is None: {nothing is None}")       # True

# Meaningful difference
score_not_taken = None
score_zero = 0            # Student took test, got 0
print(f"\nNot taken: {score_not_taken}")
print(f"Got zero: {score_zero}")
# None is different from "empty" or "zero" values
empty_string = ""
zero = 0
empty_list = []
nothing = None

print("=== Type Comparison ===")
print(f"type(''): {type(empty_string)}")
print(f"type(0): {type(zero)}")
print(f"type([]): {type(empty_list)}")
print(f"type(None): {type(nothing)}")

print("\n=== Truthiness ===")
print(f"bool(''): {bool(empty_string)}")    # False (falsy)
print(f"bool(0): {bool(zero)}")              # False (falsy)
print(f"bool([]): {bool(empty_list)}")       # False (falsy)
print(f"bool(None): {bool(nothing)}")        # False (falsy)

print("\n=== Identity ===")
print(f"'' is None: {empty_string is None}")   # False
print(f"0 is None: {zero is None}")             # False
print(f"[] is None: {empty_list is None}")      # False
print(f"None is None: {nothing is None}")       # True

# Meaningful difference
score_not_taken = 0
score_zero = 0            # Student took test, got 0
print(f"\nNot taken: {score_not_taken}")
print(f"Got zero: {score_zero}")
  1. empty_string ← (empty), zero ← 0, empty_list ← [], nothing ← None

    1# None is different from "empty" or "zero" values2empty_string→ (empty) = ""3zero→ 0 = 04empty_list→ [] = []5nothing→ None = None67print("=== Type Comparison ===")8print(f"type(''): {type(empty_string(empty))}")9print(f"type(0): {type(zero0)}")10print(f"type([]): {type(empty_list[])}")11print(f"type(None): {type(nothingNone)}")1213print("\n=== Truthiness ===")14print(f"bool(''): {bool(empty_string(empty))}")    # False (falsy)15print(f"bool(0): {bool(zero0)}")              # False (falsy)16print(f"bool([]): {bool(empty_list[])}")       # False (falsy)17print(f"bool(None): {bool(nothingNone)}")        # False (falsy)1819print("\n=== Identity ===")20print(f"'' is None: {empty_string(empty) is None}")   # False21print(f"0 is None: {zero0 is None}")             # False22print(f"[] is None: {empty_list[] is None}")      # False23print(f"None is None: {nothingNone is None}")       # True2425# Meaningful difference26score_not_taken→ None = None    #@score_not_taken=027score_zero→ 0 = 0            # Student took test, got 028print(f"\nNot taken: {score_not_takenNone}")29print(f"Got zero: {score_zero0}")
    output=== Type Comparison ===
    type(''): <class 'str'>
    type(0): <class 'int'>
    type([]): <class 'list'>
    type(None): <class 'NoneType'>
    
    === Truthiness ===
    bool(''): False
    bool(0): False
    bool([]): False
    bool(None): False
    
    === Identity ===
    '' is None: False
    0 is None: False
    [] is None: False
    None is None: True
    
    Not taken: None
    Got zero: 0
  1. empty_string ← (empty), zero ← 0, empty_list ← [], nothing ← None

    1# None is different from "empty" or "zero" values2empty_string→ (empty) = ""3zero→ 0 = 04empty_list→ [] = []5nothing→ None = None67print("=== Type Comparison ===")8print(f"type(''): {type(empty_string(empty))}")9print(f"type(0): {type(zero0)}")10print(f"type([]): {type(empty_list[])}")11print(f"type(None): {type(nothingNone)}")1213print("\n=== Truthiness ===")14print(f"bool(''): {bool(empty_string(empty))}")    # False (falsy)15print(f"bool(0): {bool(zero0)}")              # False (falsy)16print(f"bool([]): {bool(empty_list[])}")       # False (falsy)17print(f"bool(None): {bool(nothingNone)}")        # False (falsy)1819print("\n=== Identity ===")20print(f"'' is None: {empty_string(empty) is None}")   # False21print(f"0 is None: {zero0 is None}")             # False22print(f"[] is None: {empty_list[] is None}")      # False23print(f"None is None: {nothingNone is None}")       # True2425# Meaningful difference26score_not_taken→ 0 = 027score_zero→ 0 = 0            # Student took test, got 028print(f"\nNot taken: {score_not_taken0}")29print(f"Got zero: {score_zero0}")
    output=== Type Comparison ===
    type(''): <class 'str'>
    type(0): <class 'int'>
    type([]): <class 'list'>
    type(None): <class 'NoneType'>
    
    === Truthiness ===
    bool(''): False
    bool(0): False
    bool([]): False
    bool(None): False
    
    === Identity ===
    '' is None: False
    0 is None: False
    [] is None: False
    None is None: True
    
    Not taken: 0
    Got zero: 0

Use None when there's truly no value, not just an empty or zero value.

None Python's null/nil value. Only one None exists (singleton).

Optional return values

Functions can return None to indicate "no result found".

example
optional_return.py
Replay: real traced execution (multi-file project)
def find_index(items, target):
    """Find target in list, return index or None if not found."""
    for i, item in enumerate(items):
        if item == target:
            return i
    return None

def get_user_name(user_id):
    """Look up user, return name or None if not found."""
    users = {1: "Alice", 2: "Bob", 3: "Charlie"}
    return users.get(user_id)  # dict.get returns None for missing keys

# Using functions with optional returns
numbers = [10, 20, 30, 40, 50]
target = 30

result = find_index(numbers, target)
if result is not None:
    print(f"Found {target} at index {result}")
else:
    print(f"{target} not found")

# Looking up users
user_id = 2
name = get_user_name(user_id)
if name is not None:
    print(f"User {user_id}: {name}")
else:
    print(f"User {user_id} not found")

def find_index(items, target):
    """Find target in list, return index or None if not found."""
    for i, item in enumerate(items):
        if item == target:
            return i
    return None

def get_user_name(user_id):
    """Look up user, return name or None if not found."""
    users = {1: "Alice", 2: "Bob", 3: "Charlie"}
    return users.get(user_id)  # dict.get returns None for missing keys

# Using functions with optional returns
numbers = [5, 10, 15, 20]
target = 30

result = find_index(numbers, target)
if result is not None:
    print(f"Found {target} at index {result}")
else:
    print(f"{target} not found")

# Looking up users
user_id = 2
name = get_user_name(user_id)
if name is not None:
    print(f"User {user_id}: {name}")
else:
    print(f"User {user_id} not found")

def find_index(items, target):
    """Find target in list, return index or None if not found."""
    for i, item in enumerate(items):
        if item == target:
            return i
    return None

def get_user_name(user_id):
    """Look up user, return name or None if not found."""
    users = {1: "Alice", 2: "Bob", 3: "Charlie"}
    return users.get(user_id)  # dict.get returns None for missing keys

# Using functions with optional returns
numbers = [10, 20, 30, 40, 50]
target = 25

result = find_index(numbers, target)
if result is not None:
    print(f"Found {target} at index {result}")
else:
    print(f"{target} not found")

# Looking up users
user_id = 2
name = get_user_name(user_id)
if name is not None:
    print(f"User {user_id}: {name}")
else:
    print(f"User {user_id} not found")

def find_index(items, target):
    """Find target in list, return index or None if not found."""
    for i, item in enumerate(items):
        if item == target:
            return i
    return None

def get_user_name(user_id):
    """Look up user, return name or None if not found."""
    users = {1: "Alice", 2: "Bob", 3: "Charlie"}
    return users.get(user_id)  # dict.get returns None for missing keys

# Using functions with optional returns
numbers = [10, 20, 30, 40, 50]
target = 30

result = find_index(numbers, target)
if result is not None:
    print(f"Found {target} at index {result}")
else:
    print(f"{target} not found")

# Looking up users
user_id = 1
name = get_user_name(user_id)
if name is not None:
    print(f"User {user_id}: {name}")
else:
    print(f"User {user_id} not found")

def find_index(items, target):
    """Find target in list, return index or None if not found."""
    for i, item in enumerate(items):
        if item == target:
            return i
    return None

def get_user_name(user_id):
    """Look up user, return name or None if not found."""
    users = {1: "Alice", 2: "Bob", 3: "Charlie"}
    return users.get(user_id)  # dict.get returns None for missing keys

# Using functions with optional returns
numbers = [10, 20, 30, 40, 50]
target = 30

result = find_index(numbers, target)
if result is not None:
    print(f"Found {target} at index {result}")
else:
    print(f"{target} not found")

# Looking up users
user_id = 99
name = get_user_name(user_id)
if name is not None:
    print(f"User {user_id}: {name}")
else:
    print(f"User {user_id} not found")

  1. numbers ← [10, 20, 30, 40, 50], target ← 30

    13# Using functions with optional returns14numbers→ [10, 20, 30, 40, 50] = [10, 20, 30, 40, 50]  #@numbers=[5, 10, 15, 20]15target→ 30 = 30  #@target=25, 301617result = find_index(numbers[10, 20, 30, 40, 50], target30)18if result is not None:
  2. def find_index(items, target):

    1def find_index(items[10, 20, 30, 40, 50], target30):2    """Find target in list, return index or None if not found."""3    for i, item in enumerate(items):
  3. for i, item in enumerate(items):

    pass 1 of 3
    2"""Find target in list, return index or None if not found."""3for i0, item10 in enumerate(items[10, 20, 30, 40, 50]):4    if item == target:5        return i
    All 3 passes — pass 1 is the card above
    passiitemtarget
    1010
    2120
    323030
  4. if item == target:

    3for i, item in enumerate(items):4    if item30 == target30:5        return i26return None  #?return_none
  5. result ← 2

    17result→ 2 = find_index(numbers[10, 20, 30, 40, 50], target30)18if result is not None:
  6. if result is not None:

    17result = find_index(numbers, target)18if result2 is not None:19    print(f"Found {target30} at index {result2}")20else:
    outputFound 30 at index 2
  7. user_id ← 2

    23# Looking up users24user_id→ 2 = 2  #@user_id=1, 9925name = get_user_name(user_id2)26if name is not None:
  8. users ← {1: 'Alice', 2: 'Bob', 3: 'Charlie'}

    8def get_user_name(user_id2):9    """Look up user, return name or None if not found."""10    users→ {1: 'Alice', 2: 'Bob', 3: 'Charlie'} = {1: "Alice", 2: "Bob", 3: "Charlie"}11    return users{1: 'Alice', 2: 'Bob', 3: 'Charlie'}.get(user_id2)  # dict.get returns None for missing keys
  9. name ← Bob

    24user_id = 2  #@user_id=1, 9925name→ Bob = get_user_name(user_id2)26if name is not None:
  10. if name is not None:

    25name = get_user_name(user_id)26if nameBob is not None:27    print(f"User {user_id2}: {nameBob}")28else:
    outputUser 2: Bob
  1. numbers ← [5, 10, 15, 20], target ← 30

    13# Using functions with optional returns14numbers→ [5, 10, 15, 20] = [5, 10, 15, 20]15target→ 30 = 301617result = find_index(numbers[5, 10, 15, 20], target30)18if result is not None:
  2. def find_index(items, target):

    1def find_index(items[5, 10, 15, 20], target30):2    """Find target in list, return index or None if not found."""3    for i, item in enumerate(items):
  3. for i, item in enumerate(items):

    pass 1 of 4
    2"""Find target in list, return index or None if not found."""3for i0, item5 in enumerate(items[5, 10, 15, 20]):4    if item == target:5        return i
    All 4 passes — pass 1 is the card above
    passiitem
    105
    2110
    3215
    4320
  4. return None

    5        return i6return None
  5. result ← None

    17result→ None = find_index(numbers[5, 10, 15, 20], target30)18if result is not None:
  6. else:

    18if result is not None:19    print(f"Found {target} at index {result}")20else:21    print(f"{target30} not found")
    output30 not found
  7. user_id ← 2

    23# Looking up users24user_id→ 2 = 225name = get_user_name(user_id2)26if name is not None:
  8. users ← {1: 'Alice', 2: 'Bob', 3: 'Charlie'}

    8def get_user_name(user_id2):9    """Look up user, return name or None if not found."""10    users→ {1: 'Alice', 2: 'Bob', 3: 'Charlie'} = {1: "Alice", 2: "Bob", 3: "Charlie"}11    return users{1: 'Alice', 2: 'Bob', 3: 'Charlie'}.get(user_id2)  # dict.get returns None for missing keys
  9. name ← Bob

    24user_id = 225name→ Bob = get_user_name(user_id2)26if name is not None:
  10. if name is not None:

    25name = get_user_name(user_id)26if nameBob is not None:27    print(f"User {user_id2}: {nameBob}")28else:
    outputUser 2: Bob
  1. numbers ← [10, 20, 30, 40, 50], target ← 25

    13# Using functions with optional returns14numbers→ [10, 20, 30, 40, 50] = [10, 20, 30, 40, 50]15target→ 25 = 251617result = find_index(numbers[10, 20, 30, 40, 50], target25)18if result is not None:
  2. def find_index(items, target):

    1def find_index(items[10, 20, 30, 40, 50], target25):2    """Find target in list, return index or None if not found."""3    for i, item in enumerate(items):
  3. for i, item in enumerate(items):

    pass 1 of 5
    2"""Find target in list, return index or None if not found."""3for i0, item10 in enumerate(items[10, 20, 30, 40, 50]):4    if item == target:5        return i
    All 5 passes — pass 1 is the card above
    passiitem
    1010
    2120
    3230
    4340
    5450
  4. return None

    5        return i6return None
  5. result ← None

    17result→ None = find_index(numbers[10, 20, 30, 40, 50], target25)18if result is not None:
  6. else:

    18if result is not None:19    print(f"Found {target} at index {result}")20else:21    print(f"{target25} not found")
    output25 not found
  7. user_id ← 2

    23# Looking up users24user_id→ 2 = 225name = get_user_name(user_id2)26if name is not None:
  8. users ← {1: 'Alice', 2: 'Bob', 3: 'Charlie'}

    8def get_user_name(user_id2):9    """Look up user, return name or None if not found."""10    users→ {1: 'Alice', 2: 'Bob', 3: 'Charlie'} = {1: "Alice", 2: "Bob", 3: "Charlie"}11    return users{1: 'Alice', 2: 'Bob', 3: 'Charlie'}.get(user_id2)  # dict.get returns None for missing keys
  9. name ← Bob

    24user_id = 225name→ Bob = get_user_name(user_id2)26if name is not None:
  10. if name is not None:

    25name = get_user_name(user_id)26if nameBob is not None:27    print(f"User {user_id2}: {nameBob}")28else:
    outputUser 2: Bob
  1. numbers ← [10, 20, 30, 40, 50], target ← 30

    13# Using functions with optional returns14numbers→ [10, 20, 30, 40, 50] = [10, 20, 30, 40, 50]15target→ 30 = 301617result = find_index(numbers[10, 20, 30, 40, 50], target30)18if result is not None:
  2. def find_index(items, target):

    1def find_index(items[10, 20, 30, 40, 50], target30):2    """Find target in list, return index or None if not found."""3    for i, item in enumerate(items):
  3. for i, item in enumerate(items):

    pass 1 of 3
    2"""Find target in list, return index or None if not found."""3for i0, item10 in enumerate(items[10, 20, 30, 40, 50]):4    if item == target:5        return i
    All 3 passes — pass 1 is the card above
    passiitemtarget
    1010
    2120
    323030
  4. if item == target:

    3for i, item in enumerate(items):4    if item30 == target30:5        return i26return None
  5. result ← 2

    17result→ 2 = find_index(numbers[10, 20, 30, 40, 50], target30)18if result is not None:
  6. if result is not None:

    17result = find_index(numbers, target)18if result2 is not None:19    print(f"Found {target30} at index {result2}")20else:
    outputFound 30 at index 2
  7. user_id ← 1

    23# Looking up users24user_id→ 1 = 125name = get_user_name(user_id1)26if name is not None:
  8. users ← {1: 'Alice', 2: 'Bob', 3: 'Charlie'}

    8def get_user_name(user_id1):9    """Look up user, return name or None if not found."""10    users→ {1: 'Alice', 2: 'Bob', 3: 'Charlie'} = {1: "Alice", 2: "Bob", 3: "Charlie"}11    return users{1: 'Alice', 2: 'Bob', 3: 'Charlie'}.get(user_id1)  # dict.get returns None for missing keys
  9. name ← Alice

    24user_id = 125name→ Alice = get_user_name(user_id1)26if name is not None:
  10. if name is not None:

    25name = get_user_name(user_id)26if nameAlice is not None:27    print(f"User {user_id1}: {nameAlice}")28else:
    outputUser 1: Alice
  1. numbers ← [10, 20, 30, 40, 50], target ← 30

    13# Using functions with optional returns14numbers→ [10, 20, 30, 40, 50] = [10, 20, 30, 40, 50]15target→ 30 = 301617result = find_index(numbers[10, 20, 30, 40, 50], target30)18if result is not None:
  2. def find_index(items, target):

    1def find_index(items[10, 20, 30, 40, 50], target30):2    """Find target in list, return index or None if not found."""3    for i, item in enumerate(items):
  3. for i, item in enumerate(items):

    pass 1 of 3
    2"""Find target in list, return index or None if not found."""3for i0, item10 in enumerate(items[10, 20, 30, 40, 50]):4    if item == target:5        return i
    All 3 passes — pass 1 is the card above
    passiitemtarget
    1010
    2120
    323030
  4. if item == target:

    3for i, item in enumerate(items):4    if item30 == target30:5        return i26return None
  5. result ← 2

    17result→ 2 = find_index(numbers[10, 20, 30, 40, 50], target30)18if result is not None:
  6. if result is not None:

    17result = find_index(numbers, target)18if result2 is not None:19    print(f"Found {target30} at index {result2}")20else:
    outputFound 30 at index 2
  7. user_id ← 99

    23# Looking up users24user_id→ 99 = 9925name = get_user_name(user_id99)26if name is not None:
  8. users ← {1: 'Alice', 2: 'Bob', 3: 'Charlie'}

    8def get_user_name(user_id99):9    """Look up user, return name or None if not found."""10    users→ {1: 'Alice', 2: 'Bob', 3: 'Charlie'} = {1: "Alice", 2: "Bob", 3: "Charlie"}11    return users{1: 'Alice', 2: 'Bob', 3: 'Charlie'}.get(user_id99)  # dict.get returns None for missing keys
  9. name ← None

    24user_id = 9925name→ None = get_user_name(user_id99)26if name is not None:
  10. else:

    26if name is not None:27    print(f"User {user_id}: {name}")28else:29    print(f"User {user_id99} not found")
    outputUser 99 not found

None as a return value means the operation didn't produce a result.

optional A value that might be None - common for search results, lookups.

Default parameter values

Use None as a default when you can't use a mutable default.

defaults.py
Replay: real traced execution (multi-file project)
# BAD: Mutable default argument (don't do this!)
def bad_append(item, items=[]):
    items.append(item)
    return items

# GOOD: Use None as default, create new list inside
def good_append(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

# Demonstrate the problem
print("=== Bad function (shared list!) ===")
result1 = bad_append(1)
print(f"First call: {result1}")
result2 = bad_append(2)
print(f"Second call: {result2}")  # Contains both! Bug!

print("\n=== Good function (fresh list each time) ===")
result3 = good_append(1)
print(f"First call: {result3}")
result4 = good_append(2)
print(f"Second call: {result4}")  # Only has 2

# Practical example: optional timestamp
from datetime import datetime

def log_message(message, timestamp=None):
    if timestamp is None:
        timestamp = datetime(2025, 1, 15, 10, 30)
    print(f"[{timestamp}] {message}")

log_message("Hello")
log_message("Custom time", datetime(2024, 1, 1, 12, 0))

  1. print("=== Bad function (shared list!) ===")

    13# Demonstrate the problem14print("=== Bad function (shared list!) ===")15result1 = bad_append(1)16print(f"First call: {result1}")
    output=== Bad function (shared list!) ===
  2. items ← [1]

    pass 1 of 2
    1# BAD: Mutable default argument (don't do this!)2def bad_append(item1, items[]=[]):  #?mutable_default3    items→ [1].append(item1)4    return items[1]
  3. result1 ← [1]

    14print("=== Bad function (shared list!) ===")15result1→ [1] = bad_append(1)16print(f"First call: {result1[1]}")17result2 = bad_append(2)18print(f"Second call: {result2}")  # Contains both! Bug!
    outputFirst call: [1]
  4. items ← [1, 2]

    pass 2 of 2
    1# BAD: Mutable default argument (don't do this!)2def bad_append(item2, items[1]=[]):  #?mutable_default3    items→ [1, 2].append(item2)4    return items[1, 2]
  5. result2 ← [1, 2]

    16print(f"First call: {result1}")17result2→ [1, 2] = bad_append(2)18print(f"Second call: {result2[1, 2]}")  # Contains both! Bug!1920print("\n=== Good function (fresh list each time) ===")21result3 = good_append(1)22print(f"First call: {result3}")
    outputSecond call: [1, 2]
    
    === Good function (fresh list each time) ===
  6. def good_append(item, items=None):

    pass 1 of 2
    6# GOOD: Use None as default, create new list inside7def good_append(item1, itemsNone=NoneNone):8    if items is None:9        items = []
  7. items ← []

    pass 1 of 2
    7def good_append(item, items=None):8    if itemsNone is None:9        items→ [] = []10    items.append(item)
  8. items ← [1]

    9    items = []10items→ [1].append(item1)11return items[1]
  9. result3 ← [1]

    20print("\n=== Good function (fresh list each time) ===")21result3→ [1] = good_append(1)22print(f"First call: {result3[1]}")23result4 = good_append(2)24print(f"Second call: {result4}")  # Only has 2
    outputFirst call: [1]
  10. def good_append(item, items=None):

    pass 2 of 2
    6# GOOD: Use None as default, create new list inside7def good_append(item2, itemsNone=NoneNone):8    if items is None:9        items = []
  11. items ← []

    pass 2 of 2
    7def good_append(item, items=None):8    if itemsNone is None:9        items→ [] = []10    items.append(item)
  12. items ← [2]

    9    items = []10items→ [2].append(item2)11return items[2]
  13. result4 ← [2]

    22print(f"First call: {result3}")23result4→ [2] = good_append(2)24print(f"Second call: {result4[2]}")  # Only has 22526# Practical example: optional timestamp27from datetime import datetime2829def log_message(message, timestamp=None):30    if timestamp is None:31        timestamp = datetime(2025, 1, 15, 10, 30)32    print(f"[{timestamp}] {message}")3334log_message("Hello")35log_message("Custom time", datetime(2024, 1, 1, 12, 0))
    outputSecond call: [2]
  14. def log_message(message, timestamp=None):

    pass 1 of 2
    29def log_message(messageHello, timestampNone=NoneNone):30    if timestamp is None:31        timestamp = datetime(2025, 1, 15, 10, 30)
  15. timestamp ← 2025-01-15 10:30:00

    29def log_message(message, timestamp=None):30    if timestampNone is None:31        timestamp→ 2025-01-15 10:30:00 = datetime(2025, 1, 15, 10, 30)32    print(f"[{timestamp}] {message}")
  16. print(f"[{timestamp}] {message}")

    31    timestamp = datetime(2025, 1, 15, 10, 30)32print(f"[{timestamp2025-01-15 10:30:00}] {messageHello}")
    output[2025-01-15 10:30:00] Hello
  17. log_message("Hello")

    34log_message("Hello")35log_message("Custom time", datetime(2024, 1, 1, 12, 0))
  18. def log_message(message, timestamp=None):

    pass 2 of 2
    29def log_message(messageCustom time, timestamp2024-01-01 12:00:00=NoneNone):30    if timestamp is None:31        timestamp = datetime(2025, 1, 15, 10, 30)32    print(f"[{timestamp2024-01-01 12:00:00}] {messageCustom time}")
    output[2024-01-01 12:00:00] Custom time
  19. log_message("Custom time", datetime(2024, 1, 1, 12, 0))

    34log_message("Hello")35log_message("Custom time", datetime(2024, 1, 1, 12, 0))

Never use [] or {} as default parameters - use None instead.

sentinel Special value indicating "not set" - None is Python's built-in sentinel.

Checking for None

Use is None or is not None for explicit None checks.

example
checking.py
Replay: real traced execution (multi-file project)
value = None

# Preferred: identity check with 'is'
if value is None:
    print("value is None")
else:
    print(f"value is: {value}")

# Also valid for 'not None'
if value is not None:
    print("value exists")
else:
    print("value is missing")

# Why 'is' instead of '=='?
print("\n=== is vs == ===")
x = None
print(f"x is None: {x is None}")     # True - identity
print(f"x == None: {x == None}")     # True - equality

# 'is' is faster and clearer for None
# == can be overridden by custom classes

# Common pattern: guard clause
def process(data):
    if data is None:
        print("No data to process")
        return
    print(f"Processing: {data}")

process(None)
process("some data")

# Truthiness vs explicit None check
name = ""

# These behave differently!
if name:  # Checks truthiness - fails for empty string
    print(f"Truthy: {name}")
if name is not None:  # Checks for None specifically - passes for ""
    print(f"Not None: '{name}'")
value = "hello"

# Preferred: identity check with 'is'
if value is None:
    print("value is None")
else:
    print(f"value is: {value}")

# Also valid for 'not None'
if value is not None:
    print("value exists")
else:
    print("value is missing")

# Why 'is' instead of '=='?
print("\n=== is vs == ===")
x = None
print(f"x is None: {x is None}")     # True - identity
print(f"x == None: {x == None}")     # True - equality

# 'is' is faster and clearer for None
# == can be overridden by custom classes

# Common pattern: guard clause
def process(data):
    if data is None:
        print("No data to process")
        return
    print(f"Processing: {data}")

process(None)
process("some data")

# Truthiness vs explicit None check
name = ""

# These behave differently!
if name:  # Checks truthiness - fails for empty string
    print(f"Truthy: {name}")
if name is not None:  # Checks for None specifically - passes for ""
    print(f"Not None: '{name}'")
value = 0

# Preferred: identity check with 'is'
if value is None:
    print("value is None")
else:
    print(f"value is: {value}")

# Also valid for 'not None'
if value is not None:
    print("value exists")
else:
    print("value is missing")

# Why 'is' instead of '=='?
print("\n=== is vs == ===")
x = None
print(f"x is None: {x is None}")     # True - identity
print(f"x == None: {x == None}")     # True - equality

# 'is' is faster and clearer for None
# == can be overridden by custom classes

# Common pattern: guard clause
def process(data):
    if data is None:
        print("No data to process")
        return
    print(f"Processing: {data}")

process(None)
process("some data")

# Truthiness vs explicit None check
name = ""

# These behave differently!
if name:  # Checks truthiness - fails for empty string
    print(f"Truthy: {name}")
if name is not None:  # Checks for None specifically - passes for ""
    print(f"Not None: '{name}'")
value = None

# Preferred: identity check with 'is'
if value is None:
    print("value is None")
else:
    print(f"value is: {value}")

# Also valid for 'not None'
if value is not None:
    print("value exists")
else:
    print("value is missing")

# Why 'is' instead of '=='?
print("\n=== is vs == ===")
x = None
print(f"x is None: {x is None}")     # True - identity
print(f"x == None: {x == None}")     # True - equality

# 'is' is faster and clearer for None
# == can be overridden by custom classes

# Common pattern: guard clause
def process(data):
    if data is None:
        print("No data to process")
        return
    print(f"Processing: {data}")

process(None)
process("some data")

# Truthiness vs explicit None check
name = "Alice"

# These behave differently!
if name:  # Checks truthiness - fails for empty string
    print(f"Truthy: {name}")
if name is not None:  # Checks for None specifically - passes for ""
    print(f"Not None: '{name}'")
value = None

# Preferred: identity check with 'is'
if value is None:
    print("value is None")
else:
    print(f"value is: {value}")

# Also valid for 'not None'
if value is not None:
    print("value exists")
else:
    print("value is missing")

# Why 'is' instead of '=='?
print("\n=== is vs == ===")
x = None
print(f"x is None: {x is None}")     # True - identity
print(f"x == None: {x == None}")     # True - equality

# 'is' is faster and clearer for None
# == can be overridden by custom classes

# Common pattern: guard clause
def process(data):
    if data is None:
        print("No data to process")
        return
    print(f"Processing: {data}")

process(None)
process("some data")

# Truthiness vs explicit None check
name = None

# These behave differently!
if name:  # Checks truthiness - fails for empty string
    print(f"Truthy: {name}")
if name is not None:  # Checks for None specifically - passes for ""
    print(f"Not None: '{name}'")
  1. value ← None

    1value→ None = None  #@value="hello", 0
  2. if value is None:

    3# Preferred: identity check with 'is'4if valueNone is None:5    print("value is None")6else:
    outputvalue is None
  3. else:

    10if value is not None:11    print("value exists")12else:13    print("value is missing")
    outputvalue is missing
  4. x ← None

    15# Why 'is' instead of '=='?16print("\n=== is vs == ===")17x→ None = None18print(f"x is None: {xNone is None}")     # True - identity19print(f"x == None: {xNone == None}")     # True - equality2021# 'is' is faster and clearer for None22# == can be overridden by custom classes2324# Common pattern: guard clause25def process(data):26    if data is None:27        print("No data to process")28        return29    print(f"Processing: {data}")3031process(None)32process("some data")
    output
    === is vs == ===
    x is None: True
    x == None: True
  5. def process(data):

    pass 1 of 2
    24# Common pattern: guard clause25def process(dataNone):26    if data is None:27        print("No data to process")
  6. if data is None:

    25def process(data):26    if dataNone is None:27        print("No data to process")28        return29    print(f"Processing: {data}")
    outputNo data to process
  7. process(None)

    31process(None)32process("some data")
  8. def process(data):

    pass 2 of 2
    24# Common pattern: guard clause25def process(datasome data):26    if data is None:27        print("No data to process")28        return29    print(f"Processing: {datasome data}")
    outputProcessing: some data
  9. name ← (empty)

    31process(None)32process("some data")3334# Truthiness vs explicit None check35name→ (empty) = ""  #@name="Alice", None
  10. if name is not None: # Checks for None specifically - passes for ""

    39    print(f"Truthy: {name}")40if name(empty) is not None:  # Checks for None specifically - passes for ""41    print(f"Not None: '{name(empty)}'")
    outputNot None: ''
  1. value ← hello

    1value→ hello = "hello"
  2. else:

    4if value is None:5    print("value is None")6else:7    print(f"value is: {valuehello}")
    outputvalue is: hello
  3. if value is not None:

    9# Also valid for 'not None'10if valuehello is not None:11    print("value exists")12else:
    outputvalue exists
  4. x ← None

    15# Why 'is' instead of '=='?16print("\n=== is vs == ===")17x→ None = None18print(f"x is None: {xNone is None}")     # True - identity19print(f"x == None: {xNone == None}")     # True - equality2021# 'is' is faster and clearer for None22# == can be overridden by custom classes2324# Common pattern: guard clause25def process(data):26    if data is None:27        print("No data to process")28        return29    print(f"Processing: {data}")3031process(None)32process("some data")
    output
    === is vs == ===
    x is None: True
    x == None: True
  5. def process(data):

    pass 1 of 2
    24# Common pattern: guard clause25def process(dataNone):26    if data is None:27        print("No data to process")
  6. if data is None:

    25def process(data):26    if dataNone is None:27        print("No data to process")28        return29    print(f"Processing: {data}")
    outputNo data to process
  7. process(None)

    31process(None)32process("some data")
  8. def process(data):

    pass 2 of 2
    24# Common pattern: guard clause25def process(datasome data):26    if data is None:27        print("No data to process")28        return29    print(f"Processing: {datasome data}")
    outputProcessing: some data
  9. name ← (empty)

    31process(None)32process("some data")3334# Truthiness vs explicit None check35name→ (empty) = ""
  10. if name is not None: # Checks for None specifically - passes for ""

    39    print(f"Truthy: {name}")40if name(empty) is not None:  # Checks for None specifically - passes for ""41    print(f"Not None: '{name(empty)}'")
    outputNot None: ''
  1. value ← 0

    1value→ 0 = 0
  2. else:

    4if value is None:5    print("value is None")6else:7    print(f"value is: {value0}")
    outputvalue is: 0
  3. if value is not None:

    9# Also valid for 'not None'10if value0 is not None:11    print("value exists")12else:
    outputvalue exists
  4. x ← None

    15# Why 'is' instead of '=='?16print("\n=== is vs == ===")17x→ None = None18print(f"x is None: {xNone is None}")     # True - identity19print(f"x == None: {xNone == None}")     # True - equality2021# 'is' is faster and clearer for None22# == can be overridden by custom classes2324# Common pattern: guard clause25def process(data):26    if data is None:27        print("No data to process")28        return29    print(f"Processing: {data}")3031process(None)32process("some data")
    output
    === is vs == ===
    x is None: True
    x == None: True
  5. def process(data):

    pass 1 of 2
    24# Common pattern: guard clause25def process(dataNone):26    if data is None:27        print("No data to process")
  6. if data is None:

    25def process(data):26    if dataNone is None:27        print("No data to process")28        return29    print(f"Processing: {data}")
    outputNo data to process
  7. process(None)

    31process(None)32process("some data")
  8. def process(data):

    pass 2 of 2
    24# Common pattern: guard clause25def process(datasome data):26    if data is None:27        print("No data to process")28        return29    print(f"Processing: {datasome data}")
    outputProcessing: some data
  9. name ← (empty)

    31process(None)32process("some data")3334# Truthiness vs explicit None check35name→ (empty) = ""
  10. if name is not None: # Checks for None specifically - passes for ""

    39    print(f"Truthy: {name}")40if name(empty) is not None:  # Checks for None specifically - passes for ""41    print(f"Not None: '{name(empty)}'")
    outputNot None: ''
  1. value ← None

    1value→ None = None
  2. if value is None:

    3# Preferred: identity check with 'is'4if valueNone is None:5    print("value is None")6else:
    outputvalue is None
  3. else:

    10if value is not None:11    print("value exists")12else:13    print("value is missing")
    outputvalue is missing
  4. x ← None

    15# Why 'is' instead of '=='?16print("\n=== is vs == ===")17x→ None = None18print(f"x is None: {xNone is None}")     # True - identity19print(f"x == None: {xNone == None}")     # True - equality2021# 'is' is faster and clearer for None22# == can be overridden by custom classes2324# Common pattern: guard clause25def process(data):26    if data is None:27        print("No data to process")28        return29    print(f"Processing: {data}")3031process(None)32process("some data")
    output
    === is vs == ===
    x is None: True
    x == None: True
  5. def process(data):

    pass 1 of 2
    24# Common pattern: guard clause25def process(dataNone):26    if data is None:27        print("No data to process")
  6. if data is None:

    25def process(data):26    if dataNone is None:27        print("No data to process")28        return29    print(f"Processing: {data}")
    outputNo data to process
  7. process(None)

    31process(None)32process("some data")
  8. def process(data):

    pass 2 of 2
    24# Common pattern: guard clause25def process(datasome data):26    if data is None:27        print("No data to process")28        return29    print(f"Processing: {datasome data}")
    outputProcessing: some data
  9. name ← Alice

    31process(None)32process("some data")3334# Truthiness vs explicit None check35name→ Alice = "Alice"
  10. if name: # Checks truthiness - fails for empty string

    37# These behave differently!38if nameAlice:  # Checks truthiness - fails for empty string39    print(f"Truthy: {nameAlice}")40if name is not None:  # Checks for None specifically - passes for ""
    outputTruthy: Alice
  11. if name is not None: # Checks for None specifically - passes for ""

    39    print(f"Truthy: {name}")40if nameAlice is not None:  # Checks for None specifically - passes for ""41    print(f"Not None: '{nameAlice}'")
    outputNot None: 'Alice'
  1. value ← None

    1value→ None = None
  2. if value is None:

    3# Preferred: identity check with 'is'4if valueNone is None:5    print("value is None")6else:
    outputvalue is None
  3. else:

    10if value is not None:11    print("value exists")12else:13    print("value is missing")
    outputvalue is missing
  4. x ← None

    15# Why 'is' instead of '=='?16print("\n=== is vs == ===")17x→ None = None18print(f"x is None: {xNone is None}")     # True - identity19print(f"x == None: {xNone == None}")     # True - equality2021# 'is' is faster and clearer for None22# == can be overridden by custom classes2324# Common pattern: guard clause25def process(data):26    if data is None:27        print("No data to process")28        return29    print(f"Processing: {data}")3031process(None)32process("some data")
    output
    === is vs == ===
    x is None: True
    x == None: True
  5. def process(data):

    pass 1 of 2
    24# Common pattern: guard clause25def process(dataNone):26    if data is None:27        print("No data to process")
  6. if data is None:

    25def process(data):26    if dataNone is None:27        print("No data to process")28        return29    print(f"Processing: {data}")
    outputNo data to process
  7. process(None)

    31process(None)32process("some data")
  8. def process(data):

    pass 2 of 2
    24# Common pattern: guard clause25def process(datasome data):26    if data is None:27        print("No data to process")28        return29    print(f"Processing: {datasome data}")
    outputProcessing: some data
  9. name ← None

    31process(None)32process("some data")3334# Truthiness vs explicit None check35name→ None = None
is None Identity check: `x is None` - preferred over `x == None`

None in collections

Lists and dicts can contain None values - useful for optional fields.

in_collections.py
Replay: real traced execution (multi-file project)
# None in a list
scores = [85, None, 92, None, 78]  # Some students didn't take test
print(f"Scores: {scores}")
print(f"Length: {len(scores)}")  # 5 elements (None counts!)

# Filter out None values
valid_scores = [s for s in scores if s is not None]
print(f"Valid scores: {valid_scores}")
print(f"Average: {sum(valid_scores) / len(valid_scores)}")

# Count None values
none_count = scores.count(None)
print(f"Missing: {none_count}")

# None in dict (optional fields)
person = {
    "name": "Alice",
    "email": "alice@example.com",
    "phone": None
}

print(f"\nPerson: {person}")

# Check if field exists vs is None
print(f"'phone' in person: {'phone' in person}")  # True (key exists)
print(f"person['phone'] is None: {person['phone'] is None}")  # True (value is None)
print(f"'fax' in person: {'fax' in person}")  # False (key doesn't exist)

# Safe access with .get()
fax = person.get('fax')  # Returns None for missing key
print(f"person.get('fax'): {fax}")

  1. scores ← [85, None, 92, None, 78], valid_scores ← [85, 92, 78]

    1# None in a list2scores→ [85, None, 92, None, 78] = [85, None, 92, None, 78]  # Some students didn't take test3print(f"Scores: {scores[85, None, 92, None, 78]}")4print(f"Length: {len(scores[85, None, 92, None, 78])}")  # 5 elements (None counts!)56# Filter out None values7valid_scores→ [85, 92, 78] = [s for s in scores[85, None, 92, None, 78] if s is not None]8print(f"Valid scores: {valid_scores[85, 92, 78]}")9print(f"Average: {sum(valid_scores[85, 92, 78]) / len(valid_scores)}")1011# Count None values12none_count→ 2 = scores[85, None, 92, None, 78].count(None)13print(f"Missing: {none_count2}")1415# None in dict (optional fields)16person→ {'name': 'Alice', 'email': 'alice@example.com', 'phone': None} = {17    "name": "Alice",18    "email": "alice@example.com",19    "phone": None  #?optional_field20}2122print(f"\nPerson: {person{'name': 'Alice', 'email': 'alice@example.com', 'phone': None}}")2324# Check if field exists vs is None25print(f"'phone' in person: {'phone' in person{'name': 'Alice', 'email': 'alice@example.com', 'phone': None}}")  # True (key exists)26print(f"person['phone'] is None: {person['phone']None is None}")  # True (value is None)27print(f"'fax' in person: {'fax' in person{'name': 'Alice', 'email': 'alice@example.com', 'phone': None}}")  # False (key doesn't exist)2829# Safe access with .get()30fax→ None = person{'name': 'Alice', 'email': 'alice@example.com', 'phone': None}.get('fax')  # Returns None for missing key31print(f"person.get('fax'): {faxNone}")
    outputScores: [85, None, 92, None, 78]
    Length: 5
    Valid scores: [85, 92, 78]
    Average: 85.0
    Missing: 2
    
    Person: {'name': 'Alice', 'email': 'alice@example.com', 'phone': None}
    'phone' in person: True
    person['phone'] is None: True
    'fax' in person: False
    person.get('fax'): None

None in a list is a valid element, different from the element not existing.

Exercise: none_patterns.py

Explore common None patterns: Optional, guard clauses, null coalescing