Python’s standard
datetime
module is very powerful. However, it has a couple of annoying flaws.
Firstly, datetime
s are considered a kind of
date
1, which causes problems.
Although datetime
is a literal subclass of date
so Mypy and isinstance
believe a datetime “is” a date, you cannot substitute a datetime
for a
date
in a program without provoking errors at runtime.
To put it more precisely, here are two programs which define a function with
type annotations, that mypy finds no issues with. The first of which even
takes care to type-check its arguments at run-time. But both raise
TypeError
s at runtime:
Comparing datetime
to date
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
1 2 3 4 5 6 |
|
Comparing “naive” and “aware” datetime
:
1 2 3 4 5 6 |
|
1 2 3 4 5 6 |
|
In some sense, the whole point of using Mypy - or, indeed, of runtime
isinstance
checks - is to avoid TypeError
getting raised. You specify
all the types, the type-checker yells at you, you fix it, and then you can know
your code is not going to blow up in unexpected ways.
Of course, it’s still possible to avoid these TypeError
s with runtime
checks, but it’s tedious and annoying to need to put a check for .tzinfo is
not None
or not isinstance(..., datetime)
before every use of -
or >
.
The problem here is that datetime
is trying to represent too many things with
too few types. datetime
should not be inheriting from date
, because it
isn’t a date, which is why >
raises an exception when you compare the two.
Naive datetime
s represent an abstract representation of a hypothetical civil
time which are not necessarily
tethered to specific moments in physical time. You can’t know exactly what
time “today at 2:30 AM” is, unless you know where on earth you are and what the
rules are for daylight savings time in that place. However, you can still
talk about “2:30 AM” without reference to a time zone, and you can even say
that “3:30 AM” is “60 minutes after” that time, even if, given potential
changes to wall clock time, that may not be strictly true in one specific
place during a DST transition. Indeed, one of those times may refer to
multiple points in civil time at a particular location, when attached to
different sides of a DST
boundary.
By contrast, Aware datetimes
represent actual moments in time, as they
combine civil time with a timezone that has a defined UTC offset to interpret
them in.
These are very similar types of objects, but they are not in fact the same, given that all of their operators have slightly different (albeit closely related) semantics.
Using datetype
I created a small library, datetype
,
which is (almost) entirely type-time behavior. At runtime, despite
appearances, there are no instances of new types, not even wrappers.
Concretely, everything is a date
, time
, or datetime
from the standard
library. However, when type-checking with Mypy, you will now get errors
reported from the above scenarios if you use the types from datetype
.
Consider this example, quite similar to our first problematic example:
Comparing AwareDateTime
or NaiveDateTime
to date
:
1 2 3 4 5 6 7 8 9 10 |
|
Now, instead of type-checking cleanly, it produces this error, letting you know
that this call to is_after
will give you a TypeError
.
1 2 |
|
Similarly, attempting to compare naive and aware objects results in errors now.
We can even use the included AnyDateTime
type variable to include a bound
similar to
AnyStr
from
the standard library to make functions that can take either aware or naive
datetimes, as long as you don’t mix them up:
Comparing AwareDateTime
to NaiveDateTime
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
1 2 3 4 5 6 |
|
Telling the Difference
Although the types in datetype
are Protocol
s,
there’s a bit of included magic so that you can use them as type guards with
isinstance
like regular types. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Try it out, carefully
This library is very much alpha-quality; in the process of writing this blog
post, I made a half a dozen backwards-incompatible changes, and there are still
probably a few more left as I get feedback. But if this is a problem you’ve
had within your own codebases - ensuring that date
s and datetime
s don’t get
mixed up, or requiring that all datetime
s crossing some API boundary are
definitely aware and not naive, give it a try with pip install datetype
and
let me know if it catches any bugs!
-
But, in typical fashion, not a kind of
time
... ↩