
Pendulum – Python datetimes made easy - sdispater
https://github.com/sdispater/pendulum
======
sdispater
Pendulum is a new library for Python to ease datetimes, timedeltas and
timezones manipulation.

It is heavily inspired by
[Carbon]([http://carbon.nesbot.com](http://carbon.nesbot.com)) for PHP.

Basically, the Pendulum class is a replacement for the native datetime one
with some useful and intuitive methods, the Interval class is intended to be a
better time delta class and, finally, the Period class is a datetime-aware
timedelta.

Timezones are also easier to deal with: Pendulum will automatically normalize
your datetime to handle DST transitions for you.

    
    
        import pendulum
        
        pendulum.create(2013, 3, 31, 2, 30, 0, 0, 'Europe/Paris’)
        # 2:30 for the 31th of March 2013 does not exist
        # so pendulum will return the actual time which is 3:30+02:00
        '2013-03-31T03:30:00+02:00’
        
        dt = pendulum.create(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris’)
        '2013-03-31T01:59:59.999999+01:00’
        dt = dt.add(microseconds=1)
        '2013-03-31T03:00:00+02:00’
        dt.subtract(microseconds=1)
        '2013-03-31T01:59:59.999998+01:00’
    

To those wondering: yes I know
[Arrow]([http://crsmithdev.com/arrow/](http://crsmithdev.com/arrow/)) exists
but its flaws and strange API (you can throw almost anything at get() and it
will do its best to determine what you wanted, for instance) motivated me to
start this project. You can check why I think Arrow is flawed here:
[https://pendulum.eustace.io/faq/#why-not-
arrow](https://pendulum.eustace.io/faq/#why-not-arrow)

Link to the official documentation:
[https://pendulum.eustace.io/docs/](https://pendulum.eustace.io/docs/)

Link to the github project:
[https://github.com/sdispater/pendulum](https://github.com/sdispater/pendulum)

~~~
tedmiston
I can't decide if auto-normalizing my times to DST is a feature or anti-
feature. If I didn't know it was there, I would consider it a surprising
default.

~~~
sdispater
I see what you mean but as soon as you deal with timezone-aware datetimes you
don't really have a choice, if an hour has been skipped, it simply doesn't
exist.

What is important here, I think, is that any time arithmetic is not affected
by this normalization, so if you add an hour the difference will be an hour,
so you don't really have to think about it.

~~~
Walkman
> I see what you mean but as soon as you deal with timezone-aware datetimes
> you don't really have a choice, if an hour has been skipped, it simply
> doesn't exist.

So somebody clearly made a mistake (either the user or the programmer not
checking the input) which you can't automagically fix. Well, at least, it's a
terrible idea.

I understand the intent of helping them, but you make more harm this way.

It's also against the ZEN of Python.

~~~
lukasgraf
I tend to agree.

Obviously automatic normalisation is the correct thing to do when doing
arithmetic that crosses DST-rollover boundaries (this is what the Python
stdlib gets wrong), but I don't think it should be done (by default) upon
_creation_ with a location-based TZ specification:

pendulum.create(2016, 10, 30, 2, 30, 00, 0, 'Europe/Paris') is ambiguous, and
pendulum.create(2016, 3, 27, 2, 30, 00, 0, 'Europe/Paris') is non-existent.
pytz raises AmbiguousTimeError or NonExistentTimeError respectively in cases
where you try to construct local times like that and specifiy is_dst=None:
[http://pytz.sourceforge.net/#problems-with-
localtime](http://pytz.sourceforge.net/#problems-with-localtime)

With pendulum I don't see a possibility to enforce a "strict mode" that turns
of those automatic assumptions.

~~~
sdispater
I am not too kind on raising an error by default (which pytz does not do by
default either but rather return the pre-DST transition time).

I think the best thing to do here is keep the current behavior (post-DST) but
with an option to choose what you want exactly (post-DST, pre-DST)

------
Animats
This area is prone to severe bikeshedding. Back in 2012, I filed a Python bug,
"datetime: add ability to parse RFC 3339 dates and times"[1] RFC 3339
timestamps appear in email, RSS feeds, etc. The datetime library could output
them, but not parse them. There are at least seven parsing functions in PyPi
for them, and each has some major problem.

There have been steady discussions of this issue for almost four years now. I
dropped out years ago, but the arguments go on.

[1] [https://bugs.python.org/issue15873](https://bugs.python.org/issue15873)

~~~
fragmede
Past bikeshedding, there's also the long list of myths programmers believe
about time[1] and the crowd-sourced followup[2]. Pendulum inherits from
datetime in the stdlib, but I'm unsure how well either of those address the
issues raised (or even if it's possible - some need to be addressed by the
code that uses pendulum/datetime).

[1] [http://infiniteundo.com/post/25326999628/falsehoods-
programm...](http://infiniteundo.com/post/25326999628/falsehoods-programmers-
believe-about-time) [2] [http://infiniteundo.com/post/25509354022/more-
falsehoods-pro...](http://infiniteundo.com/post/25509354022/more-falsehoods-
programmers-believe-about-time)

~~~
Animats
Nobody has a good solution to leap seconds. 86400 seconds = 1 day is nailed
into too much software. The problem is serious enough that some operations,
including high frequency trading, are stopped around a leap second.

------
abuckenheimer
I know pandas is a bit meaty for a date time library if you don't already use
it but their Timestamp class is awesome. String parsing is a breeze, offsets
and timezones are easy and then there's a ton of support for time series.

    
    
        In [34]: pd.Timestamp('2016-08') == pd.Timestamp('2016.08') == pd.Timestamp('2016/08') == pd.Timestamp('08/2016')
        Out[34]: True
        
        In [38]: pd.Timestamp('2016') == pd.Timestamp(datetime.datetime(2016,1,1))
        Out[38]: True
        
        In [49]: pd.Timestamp('2016') + pd.offsets.MonthOffset(months=7) == pd.Timestamp('2016-08')
        Out[49]: True
        
        In [52]: pd.Timestamp.now()
        Out[52]: Timestamp('2016-08-17 08:01:07.576323')
        
        In [53]: pd.Timestamp.now() + pd.offsets.MonthBegin(normalize=True)
        Out[53]: Timestamp('2016-09-01 00:00:00')
        
    

see [http://pandas.pydata.org/pandas-
docs/stable/timeseries.html](http://pandas.pydata.org/pandas-
docs/stable/timeseries.html) for more examples

~~~
jonathanpoulter
I also really like the Timestamp class, are the obvious reasons not to use it?

~~~
djrobstep
Having pandas as a dependency?

~~~
jonathanpoulter
That doesn't seem like a terrible dependency, is that the only reason?

------
Walkman
I don't understand why this library exists. I am (literally) manipulating
datetimes for a living in a Django app this whole year and we don't even have
Arrow installed. Pytz and maybe dateutil is all you need.

Also I really hate when someone fragment the energy and their time making a
new, inferior library instead of fixing and patching the existings for basic
things like this. This way we will have a bunch of incomplete, inferior
libraries which all have quirks instead of only one really good one which
could everybody use...

~~~
pandatigox
I don't see what the big deal is. These people are spending their own time
making libraries, and not using up your time. Are you to judge how people
should make use of their time?

Second, I think it's great that people are making alternatives - it promotes a
healthier ecosystem where people can share good ideas as well.

> instead of fixing and patching the existings Finally, by your logic, the
> popular requests library shouldn't have existed and Kenneth would have been
> patching the more conventional urllib library

~~~
Walkman
> Are you to judge how people should make use of their time?

This library did not born because of fun, but because of solving a (non-
existing) problem from frustration. I would have not said one word if he was
doing this for fun or just learning or something like that.

~~~
pandatigox
What defines a problem as non-existing/existing? It's very subjective. While
it may not be for you, the author definitely saw an issue that needed to be
fixed.

Again, I'm using the example of requests: was Kennethwrong to implement
something new when there was already a standard lib?

------
koliber
Looks like a nice polished interface. I am wondering what is Pendulum's policy
on invalid input. The examples illustrate the inconsistent approach to invalid
inputs:

In some cases it guesses what you meant:

    
    
        >>> pendulum.create(2013, 3, 31, 2, 30, 0, 0, 'Europe/Paris')
        '2013-03-31T03:30:00+02:00' # 2:30 does not exist (Skipped time)
    

In other cases, it raises exceptions:

    
    
        pendulum.parse('2016-06-31')
        # ValueError: day is out of range for month

~~~
baq
+1, i'd expect a ValueError in the first example

------
forgotpwtomain
Python stdlib datetime/time/calendar libs are junk. That one constantly has to
read obscure function signatures in the docs to do rather obvious things is
just awful.

On the otherhand if you've e.g. Ruby/Rails datetime handling than you get used
to reasonable things working ( such as Time.now + 1.day ) that Arrow doesn't
handle well. As a matter of fact Arrow got rid of DT deltas and seemingly made
the situation worse.

I've only looked at the examples in the docs; but to be serious, Python should
just scrap their DT/time/calendar libs and copy Ruby verbatim. This NIH thing
needs to stop..

~~~
bjt
1.day? I confess to not being a Rubyist, but does that require monkeypatching
the base int class?

I don't see anything unreasonable about Pendulum's interface. Let's let Python
be Python and Ruby be Ruby.

~~~
Niksko
No, don't think so. I think this is a native syntax construct in Ruby.

~~~
teej
1.day is not a native Ruby concept, it is a method monkeypatched into the
Numeric class by Rails' ActiveSupport. See
[http://api.rubyonrails.org/classes/Numeric.html#method-i-
day](http://api.rubyonrails.org/classes/Numeric.html#method-i-day) and
[https://ruby-doc.org/core-2.2.0/Numeric.html](https://ruby-
doc.org/core-2.2.0/Numeric.html).

~~~
twblalock
I can see how people think it is native Ruby. Very few people use Ruby without
Rails.

------
josefdlange
What benefits does this have over
[Delorean]([http://delorean.readthedocs.io/en/latest/](http://delorean.readthedocs.io/en/latest/))?

The README for Pendulum seems to show me one feature Delorean doesn't
explicitly have -- `is_weekend()` -- otherwise these libraries are
conceptually very similar.

I do agree that this (and Delorean) is a usability improvement over `datetime`
and perhaps even Arrow (though I'm not tremendously familiar with Arrow).

~~~
ak217
So far I haven't seen either of these libraries do substantially better than
datetime+dateutil+pytz at anything they claim to be good at.

~~~
dvl
I'm happy to not have to import 3 libraries anymore (2 that aren't part of
standard library) to see if one date is greater than another or to add few
days to a date.

~~~
ak217
They both import all three under the hood.

------
tedmiston
I've been happily using django.utils.timezone for a while now and doing
everything on the backend in UTC. For any user-facing timestamps, to some
degree I'd rather keep that in the front end and a separate concern from my
API.

Not saying storing user timezone and converting on the backend is bad; but
this is simpler when localized timestamps aren't a core part of my app.

~~~
jsmeaton
Yes, always work in UTC. But you could use this library to convert user
provided naive datetimes to UTC, and datetimes coming from the database into
localised datetimes. Converting from/to timezones other than UTC in Django
isn't as nice as it could be.

~~~
masklinn
No, don't always work in UTC.

If a Samoan user told you to record an event for January 1st 2012 on January
1st 2011, if you stored the date in UTC would have reminded them on January
2nd 2012 (all local).

Because in May 2011 Samoa announced they were going to skip a local day and
move across the international date line. So 2011-12-30T09:00:00 UTC was
2011-12- _29_ T23:00:00 Pacific/Apia, but 2011-12-30T10:00:00 UTC was 2011-12-
_31_ T00:00:00 Pacific/Apia.

~~~
jsmeaton
Wow, that's a really interesting case. I guess the same thing would happen
whenever an offset was adjusted after you store a value in UTC intended for
another timezone. I don't think this is even supported by Django - all times
are converted to UTC on entry to the database.

~~~
masklinn
> Wow, that's a really interesting case. I guess the same thing would happen
> whenever an offset was adjusted after you store a value in UTC intended for
> another timezone.

Yep, or if a DST change was added or rescinded (which can happen on
surprisingly short orders, many governments love fucking up with DST, this
year 2016 we got an infirmation of Egyptian DST with _3 days_ lead time).

------
AdamN
What about precision? There is the Date object with day-precision and the
Datetime object with microsecond precision ... but nothing else. There's no
canonical way in Python with any library that I know of to say "July, 2013" or
even "12:15pm". The former will simply put in July 1st and the latter will put
in seconds and microseconds implying precision that doesn't exist.

Anybody know of an elegant solution/library or even a library that would be
open to including such a concept?

~~~
epistemenical
numpy does this, with np.datetime64('2013-07') or np.datetime64('2013-07-01
12:15'). I wouldn't recommend using it if you're not otherwise using numpy,
though.

------
savrajsingh
My experience over the last few years has been datetime > dateutil > pytz >
arrow > back to dateutil. Maybe I'll try Pendulum next. ;)

------
Shish2k
Good to see we're starting to come close to getting dates and times handled
according to the rules (I won't say sensibly, because the rules themselves
aren't sensible). But then we start over-reaching and trying to handle social
constructs built on top of dates and times, which is even more of a mess D:

Eg, spot the error:

    
    
        _weekend_days = [SATURDAY, SUNDAY]
        date.is_weekend = date.day in _weekend_days
    

Hint:
[https://en.wikipedia.org/wiki/Workweek_and_weekend#Around_th...](https://en.wikipedia.org/wiki/Workweek_and_weekend#Around_the_world)

(TIL one country doesn't even have _consecutive_ weekend days)

~~~
sdispater
I agree but this is an optional feature which might cover most of the
developers needs. But, if not, it's configurable:

    
    
        pendulum.set_weekend_days([pendulum.SUNDAY])

------
fastball
Looks clean.

I think the `in_words()` function should probably be delimited by commas
though, good to have your strings easily deconstructed into their constituent
parts.

