DateType documentation¶
What Is DateType?¶
DateType is a workaround for this
bug to demonstrate that we could
have a type-checking-time wrapper for datetime that doesn’t change (or almost
doesn’t change) the implementation, but fixes up two very annoying behaviors of
the stdlib datetime module:
a
datetimenow won’t type-check as adate- it still inherits at runtime (the implementation is, after all, not changed) but it doesn’t inherit at type-time.there are separate types for naive and aware
datetimes.
There’s a very small bit of implementation glue (concrete @classmethods for
construction on the Naive and Aware types, and a few functions that do
runtime checks to convert to/from stdlib types).
What Does It Contain?¶
After you pip install datetype, you can import the datetype module.
In that module, you will find several types, each of which is a typing.Protocol that abstractly describes an existing type within the standard library.
The first, datetype.Date, is just an abstract description of datetime.date ; it has all the same methods and attributes.
The other two, datetype.Time[TZ] and datetype.DateTime[TZ] are abstract descriptions of datetime.time and datetime.datetime respectively, both generic on a timezone type; which is to say, a subclass of datetime.tzinfo, or None.
In the two places that a timezone is used as a return value in one of these datetime types, the equivalent datetype object’s method has its TZ type precisely, rather than a union. These are:
The
.tzinfoproperty on bothDateTimeandTime, andThe
timetz()method onDateTime.
This means that, for example, if you have a datetype.DateTime[zoneinfo.ZoneInfo], you can get its timezone without checking anything, and it will type-check correctly:
from datetype import DateTime
from zoneinfo import ZoneInfo
def func(dt: DateTime[ZoneInfo]) -> None:
print(f"This datetime is in the {dt.tzinfo.key} timezone.")
By contrast, the datetime version of this:
from datetime import datetime
def func(dt: datetime) -> None:
print(f"This datetime is in the {dt.tzinfo.key} timezone.")
will result in 2 mypy errors:
Item "tzinfo" of "tzinfo | None" has no attribute "key", because the abstracttzinfotype doesn’t let you know that it’s azoneinfo.ZoneInfo, so it won’t haveZoneInfo’s custom key attribute, andItem "None" of "tzinfo | None" has no attribute "key", because the typedatetime.datetimemight always be a naive datetime with no timezone information at all.
This is how datetype lets you describe your datetime object to avoid spurious errors when you’ve already made sure that those objects definitely have a timezone already.
datetype will also help you by reporting errors any time you accidentally mix naive and aware datetimes.
For example, this program will type check cleanly according to mypy, but will result in a runtime TypeError: can't subtract offset-naive and offset-aware datetimes:
import datetime
def seconds_between(then: datetime.datetime, now: datetime.datetime) -> float:
return (now - then).total_seconds()
from time import sleep
a = datetime.datetime.now()
sleep(2)
b = datetime.datetime.now(datetime.UTC)
print(seconds_between(a, b))
The problem here is that a naive datetime and an aware datetime are not actually compatible types, despite sharing the same class in the standard library. With datetype, you would express this as such:
from datetype import DateTime
from datetime import UTC, tzinfo
def seconds_between(then: DateTime[tzinfo], now: DateTime[tzinfo]) -> float:
return (now - then).total_seconds()
from time import sleep
a = DateTime.now()
sleep(2)
b = DateTime.now(UTC)
print(seconds_between(a, b))
This version of the program will fail the same way at runtime, but, when checking with mypy, now you will get Argument 1 to "seconds_between" has incompatible type "DateTime[None]"; expected "DateTime[tzinfo]" while type checking. If we were to replace DateTime[tzinfo] with DateTime[None] to indicate that then could be a naive datetime, we would instead accurately get the error Unsupported operand types for - ("DateTime[tzinfo]" and "DateTime[None]").
How Do You Use It?¶
One way to use datetype is already shown in the example above: the classmethod constructors on DateTime and Time (i.e.: .now(...), .utcfromtimestamp(), .utcnow(), .fromtimestamp(...), .combine()) all have type hints that will give the appropriately specialized type back. So, when you can use those, it works more or less automatically.
However, in a real Python program, you are almost certainly going to need to deal with libraries whose type hints are in terms of the datetime module. To convert back and forth from those types, datetype exposes 3 additional functions:
datetype.aware(datetime|time, [type[timezone]:TZ]) -> DateType[TZ]: If you have a standard library objectdt, you can callaware(dt)to verify that it has a non-Nonetzinfo, and get back aDateTypeobject specialized on that timezone. If you pass a specific timezone type, it will verify and specialize on that exact type rather than the basetzinfo.datetype.naive(datetime|time) -> DateType[None]: If you have a standard library object with no timezone, this will verify that, and return aDateType[None].datetype.concrete(datetype.Date | datetype.DateTime | datetype.Time) -> datetime.date | datetime.datetime | datetime.time: If you have adatetypeobject of some type, this will return it as adatetimeobject instead.
Note that at runtime, these types are all the exact same classes. None of these functions actually instantiate anything new. These conversions merely verify any relevant invariants, then return the abstract type represnting those invariants.
Thus, the way that you use datetype as a library is to write all the functions in your application using these types, and then at the boundaries where you need to interact with existing Python libraries that use the standard library types, you can convert to and from them as described here.
By doing so, you will:
prevent spurious runtime errors from accidental mixing of naive and aware
datetimeobjects, instead getting helpful early warnings from your type checker, andprevent incorrect calculation surprises from accidentally using
datetimeobjects asdateobjects (sincedatetype.Datedoes not type check as adatetype.DateTimeor vice versa).
Contents: