Data Types
None Type
Representing Absence
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 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.
Optional return values
Functions can return None to indicate "no result 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")
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.
Default parameter values
Use None as a default when you can't use a mutable default.
# 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.
Checking for None
Use is None or is not None for explicit None checks.
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}'")
None in collections
Lists and dicts can contain None values - useful for optional fields.
# 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