This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: Add precision argument to datetime.now
Type: enhancement Stage:
Components: Library (Lib) Versions: Python 3.8, Python 3.7
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: barry, belopolsky, p-ganssle, tim.peters, vstinner
Priority: normal Keywords:

Created on 2018-01-09 15:46 by p-ganssle, last changed 2022-04-11 14:58 by admin.

Messages (20)
msg309703 - (view) Author: Paul Ganssle (p-ganssle) * (Python committer) Date: 2018-01-09 15:46
One thing I think that is fairly common is the desire to get the current datetime only up to a current precision, so you see a lot of things in, say, `dateutil` like this:

    dt = datetime.now().replace(hours=0, minutes=0, seconds=0, microseconds=0)

Or:

    dt = datetime.now().replace(microseconds=0)

I think it would make sense to add a `precision` keyword argument, similar to the `timespec` argument to isoformat (https://docs.python.org/3/library/datetime.html#datetime.datetime.isoformat), then you could just do:

dt = datetime.now(precision='day')

And get the current date as a datetime.
msg309704 - (view) Author: STINNER Victor (vstinner) * (Python committer) Date: 2018-01-09 15:56
> dt = datetime.now(precision='day')

Why not creating a date and then convert it to a datetime object?

> dt = datetime.now().replace(microseconds=0)

Yeah, that one annoys me as well, but I learnt the .replace(microseconds=0) hack, or how to format without microseconds.

By the way, are you aware of Python 3.7 enhancement of .isoformat(), the new timespec parameter?
https://docs.python.org/dev/library/datetime.html#datetime.datetime.isoformat
msg309706 - (view) Author: Paul Ganssle (p-ganssle) * (Python committer) Date: 2018-01-09 16:04
An alternate possibility here might be to implement either `__round__` or a `round` function in `datetime` (which would basically automatically add this precision functionality to *all* the constructors, not just now). An example implementation:

from datetime import datetime

    class Datetime(datetime):
        def __round__(self, ndigits=None):
            if ndigits is None:
                return self

            dflt_args = {
                'month': 1,
                'day': 1,
                'hour': 0,
                'minute': 0,
                'second': 0,
                'microsecond': 0
            }

            args = list(dflt_args.keys())

            if ndigits not in dflt_args:
                raise ValueError('Unknown rounding component: %s' % ndigits)

            idx = args.index(ndigits)

            return self.replace(**{arg: dflt_args[arg] for arg in args[idx:]})


It's not great that `__round__`'s argument is `ndigits`, though. If we don't want to just add a `round` method to `datetime`, another option might be to implement `__mod__` somehow, so you could do `datetime.now() % timedelta(seconds=1)`, but that seems complicated (and also doesn't let you round to the nearest month).
msg309707 - (view) Author: Paul Ganssle (p-ganssle) * (Python committer) Date: 2018-01-09 16:09
@Victor: With regards to getting a "date as datetime", that is another way to do it that I have also done in the past (and in fact it's how the new dateutil.utils.today() function is implemented: https://github.com/dateutil/dateutil/blob/master/dateutil/utils.py#L7), but it's still not particularly elegant and doesn't obviously convey what you want there.

And yes, I'm aware of timespec, I linked it in my original report (it was actually added in Python 3.6).
msg309710 - (view) Author: Barry A. Warsaw (barry) * (Python committer) Date: 2018-01-09 16:20
The .replace(microseconds=0) hack annoys me too, but I'd be happier with a simpler solution: make datetime.now() accept a microseconds parameter, so datetime.now(microseconds=0) would be equivalent to datetime.now().replace(microseconds=0)
msg309714 - (view) Author: Paul Ganssle (p-ganssle) * (Python committer) Date: 2018-01-09 16:33
@Barry I don't think it's a good idea to duplicate the `replace` functionality in `datetime` like that. I think the main problem isn't the `.replace`, it's the fact that you have to specify exactly which components you want to set to zero - to get "the beginning of this month" or "today at midnight" or "the beginning of the current hour" or "the beginning of the current minute", you have to manually replace a whole list of these components.

It doesn't help that `datetime.today()` leaks implementation details from `date.today()`, thus making it a slower, worse version of `datetime.now()`, since the overwhelmingly most common use cases for datetime rounding are probably "get today at midnight" and "get the current time with second precision". Still, I think a more general fix would be better now and in the future.

Even if we had "get today at midnight" and a `microseconds=0` argument to `datetime.now`, without a more general version of this, if (or possibly *when*) nanosecond precision is added to `datetime`, you'd now start having to add `microseconds=0, nanoseconds=0` or something to `datetime` (depending on the implementation of nanoseconds).
msg309715 - (view) Author: Alexander Belopolsky (belopolsky) * (Python committer) Date: 2018-01-09 16:49
Maybe __round__ can be generalized to take a timedelta instead of ndigits?

For some related prior art, take a look at <http://code.kx.com/q/ref/arith-integer/#xbar>.
msg309717 - (view) Author: Paul Ganssle (p-ganssle) * (Python committer) Date: 2018-01-09 16:56
I think if we're going to use `timedelta` then `__mod__` is the more appropriate option here, since it would be hard to interpret what `round(dt, timedelta(hours=2, microseconds=31))` would do.

Either __mod__ or __round__ with `timedelta` is a bit of a stretch in my opinion, and also is limited to well-defined units (and as such you can't round to the nearest month or year). I think a `round` taking either a string or an enum is the simplest, easiest to understand implementation (and/or adding a precision argument to `now` that is equivalent to `round(datetime.now(), precision)`).
msg309718 - (view) Author: Paul Ganssle (p-ganssle) * (Python committer) Date: 2018-01-09 17:02
One thing to note, the "example implementation" of __round__ above is an actual working prototype*:

>>> round(Datetime.now(), 'second')
Datetime(2018, 1, 9, 11, 59, 35)

>>> round(Datetime.now(), 'day')
Datetime(2018, 1, 9, 0, 0)

>>> round(Datetime.now(), 'minute')
Datetime(2018, 1, 9, 11, 59)


So to be clear, `ndigits` can already accept any arbitrary type, it's just the fact that it's *called* `ndigits` that may be confusing to users.

*with the exception that it has a bug, the final line should actually be:

    return self.replace(**{arg: dflt_args[arg] for arg in args[(idx+1):]})
msg309719 - (view) Author: Alexander Belopolsky (belopolsky) * (Python committer) Date: 2018-01-09 17:32
In my experience, when dealing with temporal data truncation (rounding towards -infinity) is more useful than any other form of rounding. See also issue 19475.
msg309722 - (view) Author: Paul Ganssle (p-ganssle) * (Python committer) Date: 2018-01-09 18:09
> In my experience, when dealing with temporal data truncation (rounding towards -infinity) is more useful than any other form of rounding. See also issue 19475.

Ah, I agree - if you see that's how my __round__ implementation works. I guess that's another problem with the semantics of `round` (which are assumed to round to the nearest whole number). I suppose we could implement __floor__, but then you have the counter-intuitive property that in order to get access to this method, you have to import `math.floor`.

We could add a `datetime.truncate()` method, maybe, and not try to be clever about overloading existing operations. Or punt on the idea of truncation in general and do what I proposed in the original thread and have all the truncation happen in `now`.
msg309723 - (view) Author: Barry A. Warsaw (barry) * (Python committer) Date: 2018-01-09 19:02
On Jan 9, 2018, at 08:33, Paul Ganssle <report@bugs.python.org> wrote:

> @Barry I don't think it's a good idea to duplicate the `replace` functionality in `datetime` like that. I think the main problem isn't the `.replace`, it's the fact that you have to specify exactly which components you want to set to zero - to get "the beginning of this month" or "today at midnight" or "the beginning of the current hour" or "the beginning of the current minute", you have to manually replace a whole list of these components.

I personally haven’t ever had to do anything but get rid of the microseconds field, so maybe my use case is too limited.  It’s a minor inconvenience and I don’t like having to throw away an intermediate datetime, so that’s the main thing I’d like to improve.  I’d also caution trying to get too fancy and complicated for use cases that are already supported or are rare.
msg309724 - (view) Author: Alexander Belopolsky (belopolsky) * (Python committer) Date: 2018-01-09 19:09
The problem that I have with the round/truncate proposal is that it is not general enough.  Days, hours, minutes etc. are just arbitrary intervals that became popular for obscure historical and astronomical reasons.  In practice, you are as likely to encounter 1 second timeseries as say 10 second timeseries.  Hourly timeseries are as likely to use the "top of the hour" (0 minutes) as they are to use the "bottom of the hour" (30 minutes).  In general, you want to have a kind of "snap to grid" functionality on the time axis where "the grid" is an arbitrary RRULE.

Looking at the dateutil, I don't see a truncate to rrule function.  Maybe a good starting point would be to implement that in dateutil and if some simpler pattern emerges that can be proposed for stdlib, we can discuss it then.
msg309726 - (view) Author: Paul Ganssle (p-ganssle) * (Python committer) Date: 2018-01-09 19:24
> Looking at the dateutil, I don't see a truncate to rrule function.  Maybe a good starting point would be to implement that in dateutil and if some simpler pattern emerges that can be proposed for stdlib, we can discuss it then.

I think that a "truncate to rrule" function is *way* beyond the scope of the standard library, since it would require an actual rrule implementation - about which there are still many questions. That said, to accomplish what you want, you would just use `rrule.before` (floor) or `rrule.after` (ceil):

    from dateutil.rrule import rrule, DAILY
    from datetime import datetime

    rr = rrule(freq=DAILY, byhour=15, byminute=30, dtstart=datetime(2000, 1, 1))

    print(rr.before(datetime(2011, 4, 17, 12, 18)))
    # 2011-04-16 15:30:00

    print(rr.before(datetime(2011, 4, 17, 16, 17)))
    # 2011-04-17 15:30:00


That said, I don't see why this is any different from the `timespec` option to isoformat (with the exception that this would support `day` and `month`). In fact, in Python 3.7, you can implement it in *terms* of timespec fairly easily:

    def truncate(dt, timespec):
        return dt.fromisoformat(dt.isoformat(timespec=timespec))

I think the fact that `timespec` is useful indicates why this is useful even before serialization - a lot of times you want a datetime *up to a specific precision*. I would also argue that the fact that `replace` allows you to manipulate each component individually - and the fact that replacing all elements of a datetime below a certain level is a very common idiom - demonstrates that these arbitrary truncation rulesets *are* special in that they are among the most common operations that people do.
msg309730 - (view) Author: Alexander Belopolsky (belopolsky) * (Python committer) Date: 2018-01-09 19:41
> I think that a "truncate to rrule" function is *way* beyond the scope of the standard library

I agree and I did not propose that.  What I said was that in the process of implementing truncate to rrule in dateutil you may encounter some common pattern that may benefit from a new stdlib datetime feature.

The operation that I often need is

def truncate_datetime(t:datetime, interval:timedelta, start=datetime.min) -> datetime
    """Return the largest datetime of the form start + interval * i not greater than t"""

but it is exactly the kind of one-liner that does not belong to stdlib.
msg309732 - (view) Author: Alexander Belopolsky (belopolsky) * (Python committer) Date: 2018-01-09 19:54
> replacing all elements of a datetime below a certain level is a very common idiom

This can be accomplished rather efficiently by truncating a time tuple:

>>> t = datetime.now()
>>> datetime(*t.timetuple()[:6])
datetime.datetime(2018, 1, 9, 14, 47, 12)
>>> datetime(*t.timetuple()[:5])
datetime.datetime(2018, 1, 9, 14, 47)
>>> datetime(*t.timetuple()[:4])
datetime.datetime(2018, 1, 9, 14, 0)
>>> datetime(*t.timetuple()[:3])
datetime.datetime(2018, 1, 9, 0, 0)

if you do this often, you can wrap this in a function


_PARTS = {'seconds': 6, 'minutes': 5, ...}
def truncate_to(t, timespec):
    return datetime(*t.timetuple()[:_PARTS[timespec])
msg309734 - (view) Author: Paul Ganssle (p-ganssle) * (Python committer) Date: 2018-01-09 20:16
> This can be accomplished rather efficiently by truncating a time tuple:

This will not preserve tzinfo, and (though this is not a concern unless nanosecond precision is added), I don't believe it preserves microseconds either.

That said, it's also not very readable code without a wrapper - it's not obvious that you're trying to truncate, or what level you're truncating to, just reading it. I think it's worth considering "truncate" to be a first-class operation of datetimes, since it comes up very frequently - people truncating off unnecessary microseconds from `now`, people truncating the result of `datetime.now()` to get today, people getting the beginning of a given month, etc.

Of course, then the question is just "where does this wrapper live". It can live in user code, which is probably not ideal since a bunch of people are implementing their own versions of this common operation and carrying around `utils` submodules or whatever just for this, or it can live in third party libraries like `dateutil` or `boltons`, or it can live in the standard library - where it will likely get the most optimized treatment. (And, honestly, `dateutil` would provide a version-independent backport anyway).

That said, if the answer to the above is "not in the standard library", I think it makes sense to add a precision argument to `now`, since that is probably the most common use case for this sort of truncation function - and it also makes a lot of sense to allow users to specify the precision with which they get the current time.
msg309735 - (view) Author: Alexander Belopolsky (belopolsky) * (Python committer) Date: 2018-01-09 21:06
I am about +0 on adding a keyword argument to datetime.now.  Note that as I wrote in issue 19475 (msg202242), "precision" may be a misleading name because python makes no guarantee about the precision of the computer clock.

Still, this feature is not appealing enough to try to squeeze into 3.7 release schedule.  I think this should go through a round of discussion either on datetime-sig or python-ideas.
msg309737 - (view) Author: Alexander Belopolsky (belopolsky) * (Python committer) Date: 2018-01-09 21:17
> (And, honestly, `dateutil` would provide a version-independent backport anyway).

Why not start with that?  Remember: python standard library is where code goes to die.  By implementing this feature in dateutil you will not be tied to relatively slow python release schedule.  Of course, you cannot change datetime.now from a 3rd party module, but you can provide your own dateutil.now with the desired features.

The only downside I see is that you will need to copy much of datetime.now implementation which is rather convoluted.
msg309738 - (view) Author: Paul Ganssle (p-ganssle) * (Python committer) Date: 2018-01-09 21:44
> Still, this feature is not appealing enough to try to squeeze into 3.7 release schedule.  I think this should go through a round of discussion either on datetime-sig or python-ideas.

Agreed. I will put together some summary in the next week or so and send it to one of those (probably python-ideas, I guess).

> Why not start with that?  Remember: python standard library is where code goes to die.  By implementing this feature in dateutil you will not be tied to relatively slow python release schedule.  Of course, you cannot change datetime.now from a 3rd party module, but you can provide your own dateutil.now with the desired features.

This is sort of independent of whether it is implemented in `datetime`, but it's a bit more complicated than that. I have already, for example, implemented a `today()` utility that does what `datetime.today()` should do if it weren't leaking implementation details from how `date.today()` is implemented (i.e. gives you the current date at midnight), because I think that's a very common use.

The reasons I brought this here are:

1. I would *prefer* it if what I implement is more or less in line with what is implemented in the standard library if a standard library solution is going to be provided so that my `dateutil` function is more of a version-independent backport than a "second way of doing things", so the direction that Python is going can inform how I choose to implement my `dateutil` backport.

2. The solution in `dateutil`, which provides functions and classes that manipulate `datetime`, will likely be different from how it is handled upstream, so it's worth having a discussion about what to do in the standard library - the standard library, for example, can overload existing operators on `datetime`, provide alternate constructors, or modify the arguments to existing alternate constructors. The semantics of this are somewhat different from a `dateutil` function that constructs a datetime (for example, there was a lot of discussion in `dateutil.utils.today` about what the function should be called - it would be cleanly namespaced if it were `datetime.today()`, but `today()` seems like it might return a `date`, etc).

For something like this, where `dateutil` is acting more like `boltons` in that it is providing little convenience functions and extensions to the `datetime` library, I think it makes sense to see whether this might get an implementation upstream and what that implementation might look like upstream, even if what happens is that I go off and implement something similar in `dateutil` and we give it a year or two in `pip` to see what problems arise.

I'll also note that even though `dateutil` has a less constrained release schedule and gets to backport features from later Python versions (allowing for earlier adoption), it's widely-used enough that I'm not very comfortable making backwards-incompatible releases. As a result, once an interface is released, it's more or less set, so it's more or less just as important to get this right in dateutil as it is in datetime.
History
Date User Action Args
2022-04-11 14:58:56adminsetgithub: 76703
2018-01-09 21:44:13p-gansslesetmessages: + msg309738
2018-01-09 21:17:35belopolskysetmessages: + msg309737
2018-01-09 21:06:30belopolskysetmessages: + msg309735
2018-01-09 20:16:02p-gansslesetmessages: + msg309734
2018-01-09 19:54:54belopolskysetmessages: + msg309732
2018-01-09 19:41:58belopolskysetmessages: + msg309730
2018-01-09 19:24:22p-gansslesetmessages: + msg309726
2018-01-09 19:09:27belopolskysetmessages: + msg309724
2018-01-09 19:02:46barrysetmessages: + msg309723
2018-01-09 18:09:13p-gansslesetmessages: + msg309722
2018-01-09 17:32:40belopolskysetmessages: + msg309719
2018-01-09 17:02:14p-gansslesetmessages: + msg309718
2018-01-09 16:56:48p-gansslesetmessages: + msg309717
2018-01-09 16:49:04belopolskysetmessages: + msg309715
2018-01-09 16:33:19p-gansslesetmessages: + msg309714
2018-01-09 16:20:55barrysetmessages: + msg309710
2018-01-09 16:18:49barrysetnosy: + barry
2018-01-09 16:15:15p-gansslesettype: enhancement
2018-01-09 16:09:24p-gansslesetmessages: + msg309707
2018-01-09 16:04:50p-gansslesetmessages: + msg309706
2018-01-09 15:56:42vstinnersetnosy: + vstinner
messages: + msg309704
2018-01-09 15:46:50p-gansslecreate