# Class \`date\` is unsafe in typed Python

**URL:** <https://discuss.python.org/t/class-date-is-unsafe-in-typed-python/107590>\
**Category:** Typing\
**Created:** [May 31, 2026, 1:44pm UTC](https://discuss.python.org/t/class-date-is-unsafe-in-typed-python/107590 "2026-05-31T13:44:20Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![maciek](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/maciek/32/596_2.png) [@maciek](https://discuss.python.org/u/maciek)\
**Post date:** [May 31, 2026, 1:44pm UTC](https://discuss.python.org/t/class-date-is-unsafe-in-typed-python/107590/1 "2026-05-31T13:44:20Z")

</div>

Hello,

I’d like to discuss adding an optional `mypy` feature that rejects unsafe subtype-based usage between `date` and `datetime`.

Currently the following code passes type checking but raises `TypeError` at runtime:

```python
from datetime import date, datetime

if datetime.now() < date.today():
    print("that's a surprise!")

```

This happens because `datetime` inherits from `date`, but does not fully behave as a substitutable subtype. In particular, ordering comparisons between `date` and `datetime` raise `TypeError`.

This mismatch has been a long-standing source of subtle runtime errors that type checkers currently do not catch.

I’ve opened a prototype implementation in mypy:

> <https://github.com/python/mypy/pull/20448>
>
> Fixes #9015.
> 
> This PR introduces \`safe-datetime\` error code, which makes mypy …check fail for unsafe date and datetime classes inheritance, following @JukkaL's \[comment\](https://github.com/python/mypy/issues/9015#issuecomment-646678572) in the related issue.
> 
> Before:
> \`\`\`python
> from datetime import date, datetime
> 
> if datetime.now() \< date.today(): # passing the type checking
> print("that's a surprise!")
> \`\`\`
> 
> After:
> \`\`\`python
> from datetime import date, datetime
> 
> if datetime.now() \< date.today(): # E: Unsupported operand types for \< ("datetime" and "date")
> print("that's a surprise!")
> \`\`\`
> 
> This is Copilot agent's work with my prompts' guidance. I reviewed the produced code. The change includes tests and documentation.
> 
> 
> 
> \*\*Alternatives considered\*\*
> \* remove the inheritance in the typeshed: https://github.com/python/typeshed/pull/15136 (worse as it isn't configurable)
> 
> \*\*PR history\*\*
> \* first iteration was adding \`usafe-subtype\` code for other potential cases like date/datetime
> \* I simplified the approach tackling only date/datetime case as others \[are being handled separately\](https://github.com/python/mypy/issues/9015#issuecomment-3777178312).

One design consideration is that this is not merely a check for comparisons that would fail at runtime. In practice, the implementation works by restricting use of the `datetime <: date` subtype relationship itself. Detecting only comparison operations would be incomplete and would miss many cases where the relationship has already been relied upon by the type checker.

This proposal assumes no changes to typeshed and preserves current runtime inheritance relationships.

I would appreciate feedback on the following questions:

_Poll ([view on site](https://discuss.python.org/t/class-date-is-unsafe-in-typed-python/107590/1))_

I also briefly considered a more general mechanism for handling known problematic subtype relationships (for example [`bool`/`int`](https://github.com/python/mypy/issues/8363) or [`str`/`Iterable[str]`](https://github.com/python/mypy/issues/11001); suggested by Jukka). However, similar discussions already exist for those cases, and I think it is easier to evaluate the `date`/`datetime` case on its own merits before considering a broader framework.

I would also welcome any feedback on the PR, particularly on the design and scope.

_Poll ([view on site](https://discuss.python.org/t/class-date-is-unsafe-in-typed-python/107590/1))_

> **Chronological references**
>
> - Mar 12, 2015 – first stubs for `datetime` appear in `typeshed`
> - [add some Python 2 modules · python/typeshed@231d854 · GitHub](https://github.com/python/typeshed/commit/231d854ef3320d4bf4de88570c39c973deaa2760#diff-cae53f7e4408fca6215fb7f57e2366176a8cb68d4686d8311ac5e0234866b5a3)
> 
> - Sep 15, 2015 – todo added for `datetime` class
> - “is actually subclass of date, but \_\_le\_\_, \_\_lt\_\_, \_\_ge\_\_, \_\_gt\_\_ don’t work with date.”
> - [Improve several stubs. (math, \_random, time, etc.) · python/typeshed@dd042b3 · GitHub](https://github.com/python/typeshed/commit/dd042b3255291dce0a8b7c43cdf8ecc18a76bc44#diff-e354b887a31f9f24f567408175e26b04e58600b4a816e78e25e4fe75504a5e8c)
> 
> - Sep 30, 2015 – file moved, todo updated
> - “Is a subclass of date, but this would make some types incompatible.”
> - [add (overwrite with) mypy stubs, if available · python/typeshed@337abed · GitHub](https://github.com/python/typeshed/commit/337abed05a35f0c5b67c02d42bca0af1e2adc76e#diff-dcab43edc5164d6ccc9527f61bc0bb2d55cc8861af920d2f908213757fd708b7)
> 
> - Sep 26, 2018
> - `typeshed` issue that `datetime` and `date` are incompatible
> - [A variable annotated with 'date' is not compatible with a value of type 'datetime'. · Issue #2487 · python/typeshed · GitHub](https://github.com/python/typeshed/issues/2487)
> 
> - PR following – [Make 'datetime' a subclass of 'date'. by mvaled · Pull Request #2488 · python/typeshed · GitHub](https://github.com/python/typeshed/pull/2488)
> 
> - Mar 3, 2020 – Glyph’s issue in `typeshed`
> - [datetime.datetime is not Liskov-substitutable for datetime.date, but it's typed as if it were · Issue #3805 · python/typeshed · GitHub](https://github.com/python/typeshed/issues/3805)
> 
> - Jun 17, 2020 – Glyph’s issue in `mypy`
> - [datetimes and dates are not substitutable but mypy thinks they are · Issue #9015 · python/mypy · GitHub](https://github.com/python/mypy/issues/9015)
> 
> - Dec 4, 2020 – tomasachan’s issue in `pyright`
> - [`datetime.datetime` and `datetime.date` should be incompatible, but aren't · Issue #1240 · microsoft/pyright · GitHub](https://github.com/microsoft/pyright/issues/1240)
> 
> - Dec 6, 2020 – tomasachan’s issue in `typeshed`
> - [`datetime.datetime` should not inherit `datetime.date` · Issue #4802 · python/typeshed · GitHub](https://github.com/python/typeshed/issues/4802)
> 
> - May 19, 2022 – Glyph’s workaround library
> - [GitHub - glyph/DateType: A type wrapper for the standard library `datetime` that supplies stricter checks, such as making 'datetime' not substitutable for 'date', and separating out Naive and Aware datetimes into separate, mutually-incompatible types. · GitHub](https://github.com/glyph/DateType)
> 
> - Nov 10, 2023 – new datetime library that has types without inheritance
> - [GitHub - ariebovenberg/whenever: ⏰ Modern datetime library for Python · GitHub](https://github.com/ariebovenberg/whenever)
> 
> - Feb 11, 2025 – mttbernardini’s workaround
> - [GitHub - mttbernardini/datetypes: Typed counterparts for built-in datetime module classes with zero runtime overhead · GitHub](https://github.com/mttbernardini/datetypes)
> 
> - Jun 9, 2025 – issue in `ty`
> - [datetimes and dates are not substitutable but ty thinks they are · Issue #615 · astral-sh/ty · GitHub](https://github.com/astral-sh/ty/issues/615)

---

<div class="post-metadata">

**Author:** ![maciek](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/maciek/32/596_2.png) [@maciek](https://discuss.python.org/u/maciek)\
**Post date:** [September 2, 2026, 9:20am UTC](https://discuss.python.org/t/class-date-is-unsafe-in-typed-python/107590/2 "2026-09-02T09:20:30Z")

</div>

A quick update: I’ve opened a revised mypy PR that supersedes the prototype linked above:

> <https://github.com/python/mypy/pull/21927>
>
> Towards #9015 — this covers only the operator-usage half; see "Scope and follow-…up" below.
> 
> \`datetime\` is a subclass of \`date\`, but their comparison and subtraction dunder methods are not substitutable: \`datetime.\_\_lt\_\_\` only accepts another \`datetime\`, and \`date.\_\_lt\_\_\` only accepts another \`date\`. Because of the subclass relationship, mypy currently accepts mixed \`date\`/\`datetime\` ordering comparisons and subtraction, which raise \`TypeError\` at runtime:
> 
> \`\`\`python
> from datetime import date, datetime
> 
> d = date.today()
> dt = datetime.now()
> 
> dt \< d # passes type checking today, raises TypeError at runtime
> d - dt # same
> \`\`\`
> 
> After this change:
> 
> \`\`\`python
> dt \< d # E: Unsupported operand types for \< ("datetime" and "date") \[operator\]
> d - dt # E: Unsupported operand types for - ("date" and "datetime") \[operator\]
> \`\`\`
> 
> \*\*Approach\*\*
> 
> This reports the mismatch under the existing \`operator\` error code, at each binary-operator call site in \`checkexpr.py\`, rather than changing subtype computation. Concretely: for \`\<\`, \`\<=\`, \`\>\`, \`\>=\`, and \`-\`, if the operand that would resolve to \`date\`'s dunder is satisfied by a \`datetime\` value with no override in its MRO to support it (or vice versa), it's flagged. Descendants of \`date\`/\`datetime\` are covered by walking the MRO for the first class defining the relevant dunder. Ordering-comparison handling is Python-version-aware (3.13 changed how a \`date\` subclass compares against a \`datetime\` subclass); subtraction is checked on all versions. Equality, identity, and same-static-type operations are untouched, since they don't raise \`TypeError\`.
> 
> No new error code and no new flag — this is a precision improvement to the existing, already-enabled-by-default \`operator\` check, so it applies without any opt-in.
> 
> \*\*Scope and follow-up\*\*
> 
> This only catches the mismatch at the point an unsafe operator is actually used. A \`datetime\` can still be silently narrowed to a \`date\`-only annotation at an assignment, call argument, or return — undetected here — and only surface later, in a different function, when it collides with a real \`date\` at one of these operators. That's a distinct check (assignability/narrowing rather than operator use) and I'm planning it as a separate follow-up PR with its own opt-in error code, rather than folding it in here.
> 
> \*\*Relationship to #20448\*\*
> 
> Supersedes my earlier #20448 (closed): that version changed \`is\_subtype\`/\`is\_proper\_subtype\` in \`mypy/subtypes.py\` directly, which Jukka flagged as too invasive since it's used pervasively (joins, overload resolution, variance checks), whereas this version only adds a diagnostic on top of an already-passing check and never touches subtype computation.
> 
> \*\*Alternatives considered\*\*
> 
> \- Removing the \`date\`/\`datetime\` inheritance relationship in typeshed: python/typeshed#15136 — rejected there as not configurable.
> \- A separate \`unsafe-datetime\` error code (per earlier review feedback: naming should follow the \`unused-awaitable\`/\`explicit-any\` pattern) — dropped in favor of folding into \`operator\`, per the above.
> 
> \*\*Test plan\*\*
> 
> \- New \`test-data/unit/check-unsafe-datetime.test\` covering: ordering comparisons and subtraction across \`date\`/\`datetime\`; equality/identity/same-type exemptions; \`Optional\`/bound-\`TypeVar\` narrowing; subclasses of both \`date\` and \`datetime\` (including the pre-/post-3.13 comparison behavior change); \`NewType\` wrappers; and classes that override the relevant dunder to explicitly support the mixed pair.
> \- Full \`mypy\` test suite and self-check pass.
> 
> \*\*AI usage\*\*
> That's my first mypy contribution. I used various LLM agents help while creating those changes. I understand them, did my best to review them and tested them locally myself.
> 
> cc @JukkaL @sterliakov

Following the feedback on the previous approach, this version does not change subtype checking. Instead, it detects unsafe mixed `date`/`datetime` ordering comparisons and subtraction at the point of use, under the existing `operator` check.

The broader problem of silently accepting a `datetime` where a `date` is expected is left for a separate follow-up.
