Skip to content

Detect unsafe datetime/date operators - #21927

Open
m-aciek wants to merge 1 commit into
python:masterfrom
m-aciek:safe-datetime-usage-site
Open

Detect unsafe datetime/date operators#21927
m-aciek wants to merge 1 commit into
python:masterfrom
m-aciek:safe-datetime-usage-site

Conversation

@m-aciek

@m-aciek m-aciek commented Sep 2, 2026

Copy link
Copy Markdown

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:

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:

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: Experiment: revert datetime-date inheritance 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

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.

Report these under the existing `operator` error code by checking, at
each binary operator site, whether the operand that would resolve to
date's dunder is satisfied by a datetime value with no override to
support it (and vice versa). Descendants of date/datetime are covered
by walking the MRO for the first class that defines the dunder.

Ordering-comparison handling is Python-version-aware, since 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 left untouched, since they don't raise
TypeError.

This covers only the operator-usage half of python#9015. A datetime can still
survive under a date-only annotation across an assignment, argument, or
return -- undetected here -- until it later reaches one of these
operators; a follow-up narrowing-based check is planned separately.

Towards python#9015.
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant