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.

none_vs_empty.py
# 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    # Student didn't take test
score_zero = 0            # Student took test, got 0
print(f"\nNot taken: {score_not_taken}")
print(f"Got zero: {score_zero}")

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".

optional_return.py
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 = 
target = 

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 = 
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 = 
target = 

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 = 
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 = 
target = 

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 = 
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 = 
target = 

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 = 
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 = 
target = 

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 = 
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 = 
target = 

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 = 
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 = 
target = 

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 = 
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 = 
target = 

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 = 
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 = 
target = 

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 = 
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 = 
target = 

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 = 
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 = 
target = 

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 = 
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 = 
target = 

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 = 
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")

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
# 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.now()
    print(f"[{timestamp}] {message}")

log_message("Hello")
log_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.

checking.py
value = 

# 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 = 

# 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 = 

# 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 = 

# 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 = 

# 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 = 

# 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 = 

# 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 = 

# 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 = 

# 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}'")
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
# 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}")

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