Type Hints
Optional Types (and None)
Database lookups, configuration values, and user input often produce "nothing found" results. Optional types make the possibility of None explicit in your code, forcing you to handle missing values before type checkers let you use the result.
Parsing Optional Values
optional_basics.py
# Optional basics
from typing import Optional
# Two equivalent ways
name1: Optional[str] = "Alice"
name2: str | None =
print("name1:", name1)
print("name2:", name2)
# Function returning optional
def lookup_city(user_id: int) -> str | None:
cities: dict[int, str] = {1: "Paris", 2: "Tokyo"}
return cities.get(user_id)
print("lookup_city(1) =", lookup_city(1))
print("lookup_city(99) =", lookup_city(99))
# Optional basics
from typing import Optional
# Two equivalent ways
name1: Optional[str] = "Alice"
name2: str | None =
print("name1:", name1)
print("name2:", name2)
# Function returning optional
def lookup_city(user_id: int) -> str | None:
cities: dict[int, str] = {1: "Paris", 2: "Tokyo"}
return cities.get(user_id)
print("lookup_city(1) =", lookup_city(1))
print("lookup_city(99) =", lookup_city(99))
# Optional basics
from typing import Optional
# Two equivalent ways
name1: Optional[str] = "Alice"
name2: str | None =
print("name1:", name1)
print("name2:", name2)
# Function returning optional
def lookup_city(user_id: int) -> str | None:
cities: dict[int, str] = {1: "Paris", 2: "Tokyo"}
return cities.get(user_id)
print("lookup_city(1) =", lookup_city(1))
print("lookup_city(99) =", lookup_city(99))
narrowing.py
# Narrowing optionals
def greet(name: str | None) -> str:
# Narrowing with is None
if name is None:
return "Hello, stranger"
# Here name is treated as str by type checkers
return f"Hello, {name.upper()}"
print(greet("Alice"))
print(greet(None))
# Guard function
def ensure_str(value: str | None) -> str:
if value is None:
raise ValueError("value is required")
return value
print("ensure_str('x') =", ensure_str("x"))
optional_collections.py
# Optional with collections
from typing import Optional
# dict.get returns Optional[V]
ages: dict[str, int] = {"Alice": 30, "Bob": 27}
maybe_age: Optional[int] = ages.get("Charlie")
print("maybe_age:", maybe_age)
# Provide a default to avoid Optional
age_or_zero: int = ages.get("Charlie", 0)
print("age_or_zero:", age_or_zero)
# Optional element in list
scores: list[int | None] = [10, None, 30]
# Filter out None safely
clean: list[int] = [s for s in scores if s is not None]
print("clean:", clean)
sentinel.py
# Sentinel values
from typing import Any
# Sentinel object
MISSING = object()
# None could be a valid value, so we need a different “not provided” marker.
def get_setting(settings: dict[str, Any], key: str, default: Any = MISSING) -> Any:
value = settings.get(key, MISSING)
if value is not MISSING:
return value
if default is MISSING:
raise KeyError(key)
return default
cfg: dict[str, Any] = {"timeout": None, "retries": 3}
print("timeout (explicit None) =", get_setting(cfg, "timeout"))
print("retries =", get_setting(cfg, "retries"))
print("missing with default =", get_setting(cfg, "missing", 0))
parse_optional.py
# Parsing that can fail
from typing import Optional
# Optional parse
def try_parse_int(text: str) -> Optional[int]:
try:
return int(text)
except ValueError:
return None
inputs = ["10", "x", "42"]
parsed: list[int] = []
for t in inputs:
value = try_parse_int(t)
if value is None:
print("skip:", t)
continue
parsed.append(value)
print("parsed:", parsed)
# Alternative: raise instead of Optional
def parse_int(text: str) -> int:
return int(text)
print("parse_int('7') =", parse_int("7"))
Optional[T] - a type alias meaning `T | None`, indicating the value might be missing
narrowing - using `if x is not None:` checks to prove to the type checker that a value exists
sentinel - a unique object used when `None` is a valid value and you need to distinguish "not provided"
Exercise: practical.py
Build a config loader that handles missing keys gracefully