Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
glyph opened this issue Feb 11, 2021 · 8 comments
Open
Labels

Comments

@glyph
Copy link

glyph commented Feb 11, 2021

Feature

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

  • definitely doesn't have a tzinfo
  • has a tzinfo
  • has a tzinfo that specifically is / isn't from pytz

This could be resolved by an Intersection[] type as described in #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 glyph added the feature label Feb 11, 2021
@glyph
Copy link
Author

glyph commented Feb 11, 2021

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

@ghost
Copy link

ghost commented Feb 11, 2021

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
Copy link
Author

glyph commented Feb 11, 2021

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
Copy link

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

@glyph
Copy link
Author

glyph commented Jun 12, 2022

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
Copy link
Contributor

Gobot1234 commented Mar 12, 2023

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
Copy link

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

@ncoghlan
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants