python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.42k stars 2.82k forks source link

I want to be able to declare "naive datetime" or "aware datetime" as types #10067

Open glyph opened 3 years ago

glyph commented 3 years ago

Feature

I would like to be able to have a way to say foo: datetime but know that foo either:

This could be resolved by an Intersection[] type as described in https://github.com/python/mypy/issues/2087 and a protocol that defines the tzinfo attribute appropriately.

Pitch

My database layer can only persist datetimes with a timezone; my timezone parsing code has portions where I want to make sure I haven't associated a timezone yet because to do so and then convert while replacing it would be an error. Really, naive datetimes and aware datetimes are subtly, but profoundly different types of objects, and their arithmetic represents different types of deltas.

Also, pytz requires different sorts of logic on arithmetic and comparison (i.e. possibly normalize is required) than other zoneinfo objects.

glyph commented 3 years ago

(I have to assume someone has asked for this already, but I promise I searched for it before filing!)

ghost commented 3 years ago

I happened to run into this due to the link with issue 2087. Assuming you know the data type somewhere in the process, wouldn't this be easily resolvable by using typing.NewType and typing.cast (or just# type: ignore)? As a simple example:

import datetime, typing

WithTz = typing.NewType('WithTz', datetime.datetime)
WithoutTz = typing.NewType('WithoutTz', datetime.datetime)

a: WithoutTz = typing.cast(WithoutTz, datetime.datetime(2020, 12, 21))
b: WithTz = typing.cast(WithTz, datetime.datetime(2020, 12, 21))

def test_without(c: WithoutTz): pass
def test_with(c: WithTz): pass

test_without(a)
test_without(b)
test_with(a)
test_with(b)

This would yield incompatible type errors for test_without(b) and test_with(a) while correctly letting the others pass. The only downside would be the (in my opinion) slightly ugly call to typing.cast (which you can also replace with an # type: ignore if you'd really want) anywhere you create the variable(s). And this method would avoid the need for all kinds of mypy-specific subtypes.

glyph commented 3 years ago

Assuming you know the data type somewhere in the process

That's the trick though, isn't it :-). I'll note that even in your example, you might as well have used object() since you didn't actually specify a timezone to your WithTz.

(Also, no need for cast, you can just do WithTz(datetime.datetime(...)))

this method would avoid the need for all kinds of mypy-specific subtypes

but… you just defined two mypy-specific subtypes to accomplish this?

blink1073 commented 2 years ago

What if mypy offered an overloaded datetime.__new__ that returns a tz-aware variant of the class?

glyph commented 2 years ago

I did a proof of concept here by just copying over the pyi file and starting from there, rather than trying to convince mypy to somehow transform the existing stubs (which is maybe impossible).

Gobot1234 commented 1 year ago

PEP 696 has a way to solve this using default and bound which should be timezone | None. datetime[timezone] would mean timezone-aware datetimes are expected to be passed, datetime[None] means naive datetimes and datetime[timezone | None] means you don’t care about awareness.

I actually implemented this change to my local copy of typeshed and found it surprisingly ergonomic.

andyreagan commented 8 months ago

@Gobot1234 brilliant! Could you elaborate on how you added this to your configuration?

ncoghlan commented 2 months ago

I think this is also related to the way that mypy types datetime.now().astimezone().tzinfo as tzinfo | None, since there is no way for the datetime stub to declare that datetime.astimezone() will always return a timezone-aware datetime instance.