I understand that Optional[T]
is really just Union[T, None]
and that the analogy to optionals in Rust or Swift is a limited analogy. But I want to point out why the kind of type narrowing I and others would like is distinct from other type narrowing.
Epoch Fails and the Problem with Zero
Let’s take a look at a kind of common bug that I like to call an “Epoch Fail”. This is where where some process tries to read a Unix time stamp and fails. Instead of handling that error, the buggy code treats the value as zero. But a zero Unix timestamp is the last second of 1969. We get results like
Using 0 to indicate a special case or the absence of a value leads to errors when 0 can also be a coherent value. So it is very useful to have a way to indicate that there is no value for something. None
is perfect for that.
When None is not an error
Now in that example, failing to read a timestamp is almost certainly some error that should be handled as an error. But there are cases where having no value (aka None
) is not an error.
Suppose you have an object for some sort of game state or simulation that gets updated each round of the simulation. Some object variables will depend on things from the previous round. Some might even depend on the previous two rounds. Thus those variables cannot be set to anything meaningful during __init()__
. So during init, we’d want to set those variables to None.
During each round of our game or simulation there will be some methods called that need to behave is special ways. There will be a test if a variable is None
. That case is fine and it doesn’t get helped by having an inline TypeGuard-like thing.
But there will be other functions that should assume that the parameters it is given are not not None. Suppose a probability gets updated, but doing so depends on certain search parameters and results from a previous round. Suppose we have something like
def updated_prob(probability: float, search_effectiveness: float) -> float:
...
class SearchArea:
def __init__(initial_probability: float):
self.p: float = initial_probability
self.search_effectiveness: Optional[float] = None
def conduct_search(self) -> None:
...
self.search_effectiveness = self.compute_a_lot_stuff()
...
self.p = updated_prob(self.p, self.search_effectiveness)
...
If updated_prob()
is a fairly general function that really just takes float arguments then it should be declared with the type hints of float, float
for its arguments. And while we could always add a
if self.search_effectiveness is None:
raise Exception
before the call to update_prob()
, suppose we want to call update_prob()
in a list comprehension or in an an argument to some function.
Being able to say things like
p = ...
probs_by_se = [updated_prob(p, se!) for se in effectiveness_list]
with the !
makes it much easier to write clean code and to communicate what we want to the type checker.
[Note. All of the code here is something I entered directly into the web interface for the discussion. It probably has numerous syntactic errors]