Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4c32d0c
Make datetime generic over tzinfo
srittau Apr 29, 2024
e2a3800
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 29, 2024
814df90
Ignore stubtest error
srittau Apr 29, 2024
a5492ba
Overload operators
srittau Apr 29, 2024
d176a1d
Update some type ignores
srittau Apr 29, 2024
040dbcb
Ignore some mypy errors
srittau Apr 29, 2024
7ce96ea
Make pyright a bit happier
srittau Apr 29, 2024
ce3ee77
Add another type ignore
srittau Apr 29, 2024
c59db10
Make pyright mostly happy
srittau Apr 29, 2024
c0d5cc1
Set default to `Any`
srittau Apr 29, 2024
eb4f7d8
Make both, pyright and mypy happy
srittau Apr 29, 2024
14aa5d2
Annotate datetime.today()
srittau Apr 29, 2024
d8d9d41
Use explicit datetime annotation in test
srittau Apr 29, 2024
f656dbb
Revert bound, use covariance
srittau Apr 29, 2024
25a3e48
Annotate datetime.strptime()
srittau Apr 29, 2024
ce3c470
Reshuffle annotations; use Any as default again
srittau Apr 29, 2024
b148970
Add some type ignores
srittau Apr 29, 2024
f9d5875
Merge branch 'main' into datetime-tzinfo
srittau Nov 10, 2025
605f598
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 10, 2025
510c87f
Revert unittest tests
srittau Nov 10, 2025
2b2c314
Fix a test
srittau Nov 10, 2025
cbf66a8
Add another type ignore
srittau Nov 10, 2025
4974026
Restore changes to check_unittest
srittau Nov 10, 2025
d7b69b1
Fix grouping
srittau Nov 10, 2025
9b8d191
Merge branch 'main' into datetime-tzinfo
srittau Dec 20, 2025
a903506
Merge branch 'main' into datetime-tzinfo
srittau Jan 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions stdlib/@tests/test_cases/check_datetime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from __future__ import annotations

from datetime import date, datetime, time, timedelta, timezone, tzinfo
from typing import Union, cast
from typing_extensions import Never, assert_type

UTC: timezone = timezone.utc

dt_none = cast(datetime[None], None)
dt_tz = cast(datetime[tzinfo], None)
dt_both = cast(datetime[Union[tzinfo, None]], None)

# Constructors

assert_type(datetime(2000, 1, 1), datetime[None])
assert_type(datetime(2000, 1, 1, tzinfo=None), datetime[None])
assert_type(datetime(2000, 1, 1, tzinfo=UTC), datetime[tzinfo])

assert_type(datetime.fromtimestamp(0), datetime[None])
assert_type(datetime.fromtimestamp(0, None), datetime[None])
assert_type(datetime.fromtimestamp(0, UTC), datetime[tzinfo])
assert_type(datetime.utcfromtimestamp(0), datetime[None]) # pyright: ignore[reportDeprecated]

assert_type(datetime.now(), datetime[None])
assert_type(datetime.now(None), datetime[None])
assert_type(datetime.now(UTC), datetime[tzinfo])
assert_type(datetime.today(), datetime[None])
assert_type(datetime.utcnow(), datetime[None]) # pyright: ignore[reportDeprecated]

assert_type(datetime.fromisoformat("2000-01-01"), datetime[Union[tzinfo, None]])

# Comparisons

assert_type(dt_none < dt_none, bool)
assert_type(dt_tz < dt_tz, bool)
assert_type(dt_both < dt_both, bool)

assert_type(dt_none < dt_tz, Never)
assert_type(dt_tz < dt_none, Never)
assert_type(dt_both < dt_none, bool)
assert_type(dt_both < dt_tz, bool)
assert_type(dt_none < dt_both, bool)

# Sub

assert_type(dt_none - dt_none, timedelta)
assert_type(dt_tz - dt_tz, timedelta)
assert_type(dt_both - dt_both, timedelta)

assert_type(dt_none - dt_tz, Never)
assert_type(dt_tz - dt_none, Never)
assert_type(dt_both - dt_none, timedelta)
assert_type(dt_both - dt_tz, timedelta)
assert_type(dt_none - dt_both, timedelta)
assert_type(dt_tz - dt_both, timedelta)

# Combine

assert_type(datetime.combine(date(2000, 1, 1), time(12, 0)), datetime[None])
assert_type(datetime.combine(date(2000, 1, 1), time(12, 0), tzinfo=None), datetime[None])
assert_type(datetime.combine(date(2000, 1, 1), time(12, 0), tzinfo=UTC), datetime[tzinfo])

# Replace

assert_type(dt_none.replace(year=2001), datetime[None])
assert_type(dt_none.replace(year=2001, tzinfo=None), datetime[None])
assert_type(dt_none.replace(year=2001, tzinfo=UTC), datetime[tzinfo])
assert_type(dt_tz.replace(year=2001), datetime[tzinfo])
assert_type(dt_tz.replace(year=2001, tzinfo=None), datetime[None])
assert_type(dt_tz.replace(year=2001, tzinfo=UTC), datetime[tzinfo])
assert_type(dt_both.replace(year=2001), datetime[Union[tzinfo, None]])
assert_type(dt_both.replace(year=2001, tzinfo=None), datetime[None])
assert_type(dt_both.replace(year=2001, tzinfo=UTC), datetime[tzinfo])

# Attributes

assert_type(dt_none.tzinfo, None)
assert_type(dt_tz.tzinfo, tzinfo)
assert_type(dt_both.tzinfo, Union[tzinfo, None])
4 changes: 2 additions & 2 deletions stdlib/@tests/test_cases/check_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
case.assertAlmostEqual(2.4, 2.41)
case.assertAlmostEqual(Fraction(49, 50), Fraction(48, 50))
case.assertAlmostEqual(3.14, complex(5, 6))
case.assertAlmostEqual(datetime(1999, 1, 2), datetime(1999, 1, 2, microsecond=1))
case.assertAlmostEqual(datetime(1999, 1, 2), datetime(1999, 1, 2, microsecond=1), delta=timedelta(hours=1))
case.assertAlmostEqual(datetime(1999, 1, 2), datetime(1999, 1, 2, microsecond=1), None, "foo", timedelta(hours=1))
case.assertAlmostEqual(Decimal("1.1"), Decimal("1.11"))
Expand All @@ -28,7 +29,6 @@

case.assertAlmostEqual(2.4, 2.41, places=9, delta=0.02) # type: ignore
case.assertAlmostEqual("foo", "bar") # type: ignore
case.assertAlmostEqual(datetime(1999, 1, 2), datetime(1999, 1, 2, microsecond=1)) # type: ignore
case.assertAlmostEqual(Decimal("0.4"), Fraction(1, 2)) # type: ignore
case.assertAlmostEqual(complex(2, 3), Decimal("0.9")) # type: ignore

Expand All @@ -39,12 +39,12 @@
case.assertAlmostEqual(1, 2.4)
case.assertNotAlmostEqual(Fraction(49, 50), Fraction(48, 50))
case.assertAlmostEqual(3.14, complex(5, 6))
case.assertNotAlmostEqual(datetime(1999, 1, 2), datetime(1999, 1, 2, microsecond=1))
case.assertNotAlmostEqual(datetime(1999, 1, 2), datetime(1999, 1, 2, microsecond=1), delta=timedelta(hours=1))
case.assertNotAlmostEqual(datetime(1999, 1, 2), datetime(1999, 1, 2, microsecond=1), None, "foo", timedelta(hours=1))

case.assertNotAlmostEqual(2.4, 2.41, places=9, delta=0.02) # type: ignore
case.assertNotAlmostEqual("foo", "bar") # type: ignore
case.assertNotAlmostEqual(datetime(1999, 1, 2), datetime(1999, 1, 2, microsecond=1)) # type: ignore
case.assertNotAlmostEqual(Decimal("0.4"), Fraction(1, 2)) # type: ignore
case.assertNotAlmostEqual(complex(2, 3), Decimal("0.9")) # type: ignore

Expand Down
183 changes: 163 additions & 20 deletions stdlib/datetime.pyi
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import sys
from abc import abstractmethod
from time import struct_time
from typing import ClassVar, Final, NoReturn, SupportsIndex, final, overload, type_check_only
from typing import Any, ClassVar, Final, Generic, NoReturn, SupportsIndex, TypeVar, final, overload, type_check_only
from typing_extensions import CapsuleType, Self, TypeAlias, deprecated, disjoint_base

if sys.version_info >= (3, 11):
Expand Down Expand Up @@ -242,10 +242,13 @@ class timedelta:
def __bool__(self) -> bool: ...
def __hash__(self) -> int: ...

_TzInfoT = TypeVar("_TzInfoT", bound=tzinfo | None, default=Any)

@disjoint_base
class datetime(date):
class datetime(date, Generic[_TzInfoT]):
min: ClassVar[datetime]
max: ClassVar[datetime]
@overload
def __new__(
cls,
year: SupportsIndex,
Expand All @@ -255,10 +258,40 @@ class datetime(date):
minute: SupportsIndex = 0,
second: SupportsIndex = 0,
microsecond: SupportsIndex = 0,
tzinfo: _TzInfo | None = None,
tzinfo: None = None,
*,
fold: int = 0,
) -> Self: ...
) -> datetime[None]: ...
@overload
def __new__(
cls,
year: SupportsIndex,
month: SupportsIndex,
day: SupportsIndex,
hour: SupportsIndex = 0,
minute: SupportsIndex = 0,
second: SupportsIndex = 0,
microsecond: SupportsIndex = 0,
*,
tzinfo: _TzInfo,
fold: int = 0,
) -> datetime[_TzInfo]: ...
@overload
def __new__(
cls,
year: SupportsIndex,
month: SupportsIndex,
day: SupportsIndex,
hour: SupportsIndex,
minute: SupportsIndex,
second: SupportsIndex,
microsecond: SupportsIndex,
tzinfo: _TzInfo,
*,
fold: int = 0,
) -> datetime[_TzInfo]: ...
@classmethod
def fromisoformat(cls, date_string: str, /) -> datetime[_TzInfo | None]: ... # type: ignore[override]
@property
def hour(self) -> int: ...
@property
Expand All @@ -268,29 +301,47 @@ class datetime(date):
@property
def microsecond(self) -> int: ...
@property
def tzinfo(self) -> _TzInfo | None: ...
def tzinfo(self) -> _TzInfoT: ...
@property
def fold(self) -> int: ...
# On <3.12, the name of the first parameter in the pure-Python implementation
# didn't match the name in the C implementation,
# meaning it is only *safe* to pass it as a keyword argument on 3.12+
if sys.version_info >= (3, 12):
@overload # type: ignore[override]
@classmethod
def fromtimestamp(cls, timestamp: float, tz: _TzInfo | None = None) -> Self: ...
def fromtimestamp(cls, timestamp: float, tz: None = None) -> datetime[None]: ...
@overload
@classmethod
def fromtimestamp(cls, timestamp: float, tz: _TzInfo) -> datetime[_TzInfo]: ...
else:
@overload # type: ignore[override]
@classmethod
def fromtimestamp(cls, timestamp: float, /, tz: None = None) -> datetime[None]: ...
@overload
@classmethod
def fromtimestamp(cls, timestamp: float, /, tz: _TzInfo | None = None) -> Self: ...
def fromtimestamp(cls, timestamp: float, /, tz: _TzInfo) -> datetime[_TzInfo]: ...

@classmethod
@deprecated("Use timezone-aware objects to represent datetimes in UTC; e.g. by calling .fromtimestamp(datetime.timezone.utc)")
def utcfromtimestamp(cls, t: float, /) -> Self: ...
@deprecated("Use timezone-aware objects to represent datetimes in UTC; e.g. by calling .fromtimestamp(datetime.UTC)")
def utcfromtimestamp(cls, t: float, /) -> datetime[None]: ...
@overload
@classmethod
def now(cls, tz: None = None) -> datetime[None]: ...
@overload
@classmethod
def now(cls, tz: _TzInfo) -> datetime[_TzInfo]: ...
@classmethod
def now(cls, tz: _TzInfo | None = None) -> Self: ...
def today(cls) -> datetime[None]: ... # type: ignore[override]
@classmethod
@deprecated("Use timezone-aware objects to represent datetimes in UTC; e.g. by calling .now(datetime.timezone.utc)")
def utcnow(cls) -> Self: ...
@deprecated("Use timezone-aware objects to represent datetimes in UTC; e.g. by calling .now(datetime.UTC)")
def utcnow(cls) -> datetime[None]: ...
@overload
@classmethod
def combine(cls, date: _Date, time: _Time, tzinfo: None = None) -> datetime[None]: ...
@overload
@classmethod
def combine(cls, date: _Date, time: _Time, tzinfo: _TzInfo | None = ...) -> Self: ...
def combine(cls, date: _Date, time: _Time, tzinfo: _TzInfo) -> datetime[_TzInfo]: ...
def timestamp(self) -> float: ...
def utctimetuple(self) -> struct_time: ...
def date(self) -> _Date: ...
Expand All @@ -312,6 +363,7 @@ class datetime(date):
fold: int = ...,
) -> Self: ...

@overload
def replace(
self,
year: SupportsIndex = ...,
Expand All @@ -321,25 +373,116 @@ class datetime(date):
minute: SupportsIndex = ...,
second: SupportsIndex = ...,
microsecond: SupportsIndex = ...,
tzinfo: _TzInfo | None = ...,
*,
fold: int = ...,
) -> Self: ...
@overload
def replace(
self,
year: SupportsIndex,
month: SupportsIndex,
day: SupportsIndex,
hour: SupportsIndex,
minute: SupportsIndex,
second: SupportsIndex,
microsecond: SupportsIndex,
tzinfo: _TzInfo,
*,
fold: int = ...,
) -> datetime[_TzInfo]: ...
@overload
def replace(
self,
year: SupportsIndex = ...,
month: SupportsIndex = ...,
day: SupportsIndex = ...,
hour: SupportsIndex = ...,
minute: SupportsIndex = ...,
second: SupportsIndex = ...,
microsecond: SupportsIndex = ...,
*,
tzinfo: _TzInfo,
fold: int = ...,
) -> datetime[_TzInfo]: ...
@overload
def replace(
self,
year: SupportsIndex,
month: SupportsIndex,
day: SupportsIndex,
hour: SupportsIndex,
minute: SupportsIndex,
second: SupportsIndex,
microsecond: SupportsIndex,
tzinfo: None,
*,
fold: int = ...,
) -> datetime[None]: ...
@overload
def replace(
self,
year: SupportsIndex = ...,
month: SupportsIndex = ...,
day: SupportsIndex = ...,
hour: SupportsIndex = ...,
minute: SupportsIndex = ...,
second: SupportsIndex = ...,
microsecond: SupportsIndex = ...,
*,
tzinfo: None,
fold: int = ...,
) -> datetime[None]: ...
def astimezone(self, tz: _TzInfo | None = None) -> Self: ...
def isoformat(self, sep: str = "T", timespec: str = "auto") -> str: ...
@classmethod
def strptime(cls, date_string: str, format: str, /) -> Self: ...
def strptime(cls, date_string: str, format: str, /) -> datetime[_TzInfo | None]: ... # type: ignore[override]
def utcoffset(self) -> timedelta | None: ...
def tzname(self) -> str | None: ...
def dst(self) -> timedelta | None: ...
def __le__(self, value: datetime, /) -> bool: ... # type: ignore[override]
def __lt__(self, value: datetime, /) -> bool: ... # type: ignore[override]
def __ge__(self, value: datetime, /) -> bool: ... # type: ignore[override]
def __gt__(self, value: datetime, /) -> bool: ... # type: ignore[override]
@overload # type: ignore[override]
def __le__( # type: ignore[overload-overlap]
self: datetime[_TzInfo] | datetime[_TzInfo | None], value: datetime[_TzInfo] | datetime[_TzInfo | None], /
) -> bool: ... # type: ignore[misc]
@overload
def __le__(self: datetime[None] | datetime[_TzInfo | None], value: datetime[None] | datetime[_TzInfo | None], /) -> bool: ... # type: ignore[misc]
@overload
def __le__(self: datetime[Any], value: datetime[Any], /) -> NoReturn: ...
@overload # type: ignore[override]
def __lt__( # type: ignore[overload-overlap]
self: datetime[_TzInfo] | datetime[_TzInfo | None], value: datetime[_TzInfo] | datetime[_TzInfo | None], /
) -> bool: ... # type: ignore[misc]
@overload
def __lt__(self: datetime[None] | datetime[_TzInfo | None], value: datetime[None] | datetime[_TzInfo | None], /) -> bool: ... # type: ignore[misc]
@overload
def __lt__(self: datetime[Any], value: datetime[Any], /) -> NoReturn: ...
@overload # type: ignore[override]
def __ge__( # type: ignore[overload-overlap]
self: datetime[_TzInfo] | datetime[_TzInfo | None], value: datetime[_TzInfo] | datetime[_TzInfo | None], /
) -> bool: ... # type: ignore[misc]
@overload
def __ge__(self: datetime[None] | datetime[_TzInfo | None], value: datetime[None] | datetime[_TzInfo | None], /) -> bool: ... # type: ignore[misc]
@overload
def __ge__(self: datetime[Any], value: datetime[Any], /) -> NoReturn: ...
@overload # type: ignore[override]
def __gt__( # type: ignore[overload-overlap]
self: datetime[_TzInfo] | datetime[_TzInfo | None], value: datetime[_TzInfo] | datetime[_TzInfo | None], /
) -> bool: ... # type: ignore[misc]
@overload
def __gt__(self: datetime[None] | datetime[_TzInfo | None], value: datetime[None] | datetime[_TzInfo | None], /) -> bool: ... # type: ignore[misc]
@overload
def __gt__(self: datetime[Any], value: datetime[Any], /) -> NoReturn: ...
def __eq__(self, value: object, /) -> bool: ...
def __hash__(self) -> int: ...
@overload # type: ignore[override]
def __sub__(self, value: Self, /) -> timedelta: ...
def __sub__( # type: ignore[overload-overlap]
self: datetime[_TzInfo] | datetime[_TzInfo | None], value: datetime[_TzInfo] | datetime[_TzInfo | None], /
) -> timedelta: ...
@overload
def __sub__( # type: ignore[overload-overlap]
self: datetime[None] | datetime[_TzInfo | None], value: datetime[None] | datetime[_TzInfo | None], /
) -> timedelta: ...
@overload
def __sub__(self: datetime[Any], value: datetime[Any], /) -> NoReturn: ...
@overload
def __sub__(self, value: timedelta, /) -> Self: ...

Expand Down
4 changes: 2 additions & 2 deletions stubs/python-dateutil/@tests/test_cases/check_inheritance.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ class MyDateTime(datetime):

d = MyDateTime.now()
x = d - relativedelta(days=1)
assert_type(x, MyDateTime)
assert_type(x, datetime[None])

d3 = datetime.today()
x3 = d3 - relativedelta(days=1)
assert_type(x3, datetime)
assert_type(x3, datetime[None])

d2 = date.today()
x2 = d2 - relativedelta(days=1)
Expand Down