The Story of None: Part 2 - Recognizing It
part 1 part 2 part 3 part 4 part 5 part 6
Last time...
In part 1 of the Story of None we've seen this validation function:
def validate_end_date_later_than_start(start_date, end_date):
if end_date <= start_date:
raise ValidationError(
"The end date should be later than the start date.")
We've decided that start_date or end_date may be omitted (and be
None), and that this function is therefore buggy: we'll get a
TypeError if it's called with None as one of the arguments and a
date with the other.
A type check?
One reaction to a TypeError is to do a type check to see whether the
values are really of the right types, in this case, date, and to only
proceed if they are. Something like:
if type(start_date) == date:
...
or:
if isinstance(start_date, date):
...
This works, but I think this signals the wrong thing to the reader of the code.
The reader may start wondering whether there are code paths that
generate a start_date that is not a date but something else; we seem
to be guarding against a set of other possibilities. But in reality
we're only guarding against one possibility: None.
It is a bit of cognitive load for the developer to consider None is
what we're really checking for, and that type(None) is NoneType and
that this isn't equal to the date type. It's not right to make a
developer think about stuff they don't have to think about.
Let's instead be specific, and just handle None, and avoid type
checking.
if value?
So we want to make sure that start_date and end_date are present.
That something is there. It's now very tempting to check for their
presence with a clause like this:
if start_date:
...
This again will work, at least in this particular case, where the arguments are dates.
But it won't work for other things, like a function where the arguments are numbers.
Why won't it work for numbers? Because 0 in Python evaluates to
False. And the number 0 doesn't mean that the number is omitted. So
if we were, for example, comparing start_age and end_age, where the
ages are integer numbers, we'd be in trouble if we did something like
this:
if end_age and start_age and end_age >= start_age:
raise ValidationError("End age should come before start age")
end_age could be 0, and 0 would definitely come before a
start_age of say, 10, and we still don't raise ValidationError. A
bug!
We want to reduce the burden on the developer who has to reason about
this code, if we can, we should use a pattern that works for any case
where None can be a value. Doing so will make our code be more regular
and easier to understand.
So when we can, we should explicitly compare with None.
value == None?
Don't use == None either. It will work but it'll be a tiny bit
slower and it's not the Pythonic convention. Plus if the __eq__
operator is overloaded it may dive into that, which is what you don't
really want.
Follow the convention so that other programmers will be able to read
your code more easily. In Python you compare for equality with == but
for identity with is. I won't go into the details of identity versus
equality here. Instead I'll say that you can always safely do an
identity comparison with None.
value is None
So how do you compare with None in Python? You use:
value is None
and if you want to check whether something is not None the idiom is:
value is not None
In JavaScript by the way the idiom is value === null and
value !== null, as triple comparison is identity comparison in
JavaScript.
Next time we'll apply this approach to our validation function.