Lab 1: The Singleton Proof
Task: Prove that two different variables assigned to None share the same memory address.
a = None
b = None
print(f"ID of a: {id(a)}")
print(f"ID of b: {id(b)}")
print(f"Are they the same? {a is b}")
# Output: True (They are literally the same object)
Lab 2: The Void Function
Task: Create a function that prints “Hello” but returns nothing. Capture the result in a variable and print it.
def say_hello():
print("Hello DevSecOps!")
result = say_hello()
print(f"The return value is: {result}") # Output: None
print(f"Type: {type(result)}") # Output: <class 'NoneType'>
Lab 4: Type Hinting (Modern Python)
Task: Write a function signature that accepts a string or None, and returns an integer or None.
from typing import Optional
def process_id(user_id: Optional[str]) -> Optional[int]:
if user_id is None:
return None
return int(user_id)
Lab 1: The Mutable Default Trap (Crucial for Architects)
Scenario: You want a function that adds a user to a list. If no list is provided, start a new one.
- The Trap
def bad_add_user(name, user_list=[]):
user_list.append(name)
return user_list
print(bad_add_user("Alice")) # ['Alice']
print(bad_add_user("Bob")) # ['Alice', 'Bob'] !! Wait, why is Alice here?Reason: The default list [] is created once when Python starts. All calls share it.
- The Master Solution
def good_add_user(name, user_list=None):
if user_list is None:
user_list = [] # Created FRESH every time
user_list.append(name)
return user_list
print(good_add_user("Alice")) # ['Alice']
print(good_add_user("Bob")) # ['Bob'] (Correct!)Lab 2: The Sentinel Pattern
Scenario: You need to check if a user passed a specific argument to a function, but None is a valid input (e.g., they might want to set a value to null).
- Problem: If you use
val=Noneas default, you can’t tell if the user passedNoneor if the function used the default. - Solution: Create a unique object instance
# Create a unique object in memory
MISSING = object()
def update_config(setting=MISSING):
if setting is MISSING:
print("No change requested.")
elif setting is None:
print("User explicitly deleted the setting (Set to None).")
else:
print(f"Setting updated to: {setting}")
update_config() # Output: No change requested.
update_config(None) # Output: User explicitly deleted...— CONTENT START —
Advanced Practice Labs: Python NoneType Mastery
To achieve “Master” status with None, you must understand how it interacts with data structures, control flow, and external systems like APIs or Databases. These labs focus on the subtleties that often cause bugs in production.
Lab 3: The “Safe Fetch” Pattern (Intermediate)
Scenario: You are parsing a JSON configuration file website. Some optional fields (like “port” or “timeout”) might be missing.
- The Trap: Accessing a missing key like
config["timeout"]raises aKeyErrorand crashes the app. - The Task:
- Create a dictionary:
config = {"host": "localhost", "retries": 0}. - Try to get the “timeout” value safely. If it’s missing, default to
60. - Try to get the “retries” value safely.
- Create a dictionary:
- The Twist: If you use
config.get("retries") or 5, it will erroneously return5because0is Falsy. - The Architect Solution: Explicitly check for
Nonewhen0orFalseare valid values.
Python
config = {"host": "localhost", "retries": 0}
# 1. Standard safe fetch (Good for non-zero defaults)
timeout = config.get("timeout")
if timeout is None:
timeout = 60
# 2. The "0" trap
# BAD: This will set retries to 5, because 0 is Falsy!
# retries = config.get("retries") or 5
# GOOD: Explicit None check preserves the value 0
raw_retries = config.get("retries")
retries = raw_retries if raw_retries is not None else 5
print(f"Timeout: {timeout}, Retries: {retries}")
Lab 4: The Implicit Return Bug (Debugging)
Scenario: A junior developer wrote a function to process logs. The script crashes with TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'.
- The Code:
def calculate_uptime(logs):
# ... complex logic ...
uptime = 100
print(f"Uptime calculated: {uptime}")
# Missing return statement!
result = calculate_uptime(["log1", "log2"])
total_score = result + 50 # CRASH HERE- The Concept: If a function finishes without a
returnstatement, Python implicitly returnsNone. - The Task: Identify why
resultisNoneand fix the code by replacingprintwithreturn.
Lab 5: Sanitizing API Data (Advanced)
Scenario: You receive data from an external API where missing values are sent as null (which Python parses as None). You need to perform math on this data.
- The Problem: You cannot add
None + 10. - Task: Create a list of incoming values:
data = [10, None, 20, None, 50].- Write a “List Comprehension” that filters out
Nonevalues entirely. - Write a second version that replaces
Nonewith0.
- Write a “List Comprehension” that filters out
- Code:
data = [10, None, 20, None, 50]
# Strategy 1: Filter (Remove invalid data)
clean_data = [x for x in data if x is not None]
print(f"Filtered: {clean_data}") # [10, 20, 50]
# Strategy 2: Sanitize (Replace missing with default)
# Uses ternary operator: (value IF condition ELSE default)
sanitized_data = [x if x is not None else 0 for x in data]
print(f"Sanitized: {sanitized_data}") # [10, 0, 20, 0, 50]Lab 6: The “None” vs “False” Distinction (Architect Level)
Scenario: You are building a feature flag system.
True= Feature Enabled.False= Feature Disabled.None= Feature Not configured (Use System Default).
Task: Write logic that distinguishes between “Disabled” and “Not Configured”.
- The Trap:
if not feature_flag:evaluates to True for bothFalseandNone. - Code:
def check_feature(flag_value):
if flag_value is None:
print("Status: Using System Default")
elif flag_value:
print("Status: ENABLED")
else:
# Here, flag_value is False, but NOT None
print("Status: DISABLED explicitly")
check_feature(True) # ENABLED
check_feature(False) # DISABLED explicitly
check_feature(None) # Using System DefaultLab 7: Unpacking with None (Edge Case)
Scenario: A function returns a tuple (result, error). If successful, error is None. If it fails, result is None.
- Task: Simulate a function
api_call(success)that returns("Data", None)or(None, "404 Error"). - Logic: Unpack the result and handle the error flow.
- Code:
def api_call(success):
return ("Payload", None) if success else (None, "404 Not Found")
# Happy Path
data, error = api_call(True)
if error is None:
print(f"Success: {data}")
# Error Path
data, error = api_call(False)
if error is not None:
print(f"Failed: {error}")