Working with Datetime in Python with Arrow

Introduction

Arrow is a Python module for working with date and time. Given that there are several modules that do this, most notably the built-in datetime module, what makes Arrow different?

Most notably, the library is inspired by Moment.js, a JavaScript library that overrides the default implementation of the Date/Time API.

In this guide, we'll take a look at some key features of Arrow, to see how it handles certain common tasks.

First, let's go ahead and install it:

$ pip install Arrow

The Arrow Class

The Arrow class is an implementation of the datetime interface, with additional functionalities. Also, it's timezone-aware by default - we'll go into this in a bit later, though.

You can easily create a new Arrow instance by supplying its constructor with a few arguments:

import arrow
arrow_object = arrow.Arrow(2021, 1, 1)
print(arrow_object)

This results in:

2021-01-01T00:00:00+00:00

You can also supply the hours, minutes and seconds through the constructor:

import arrow
arrow_object = arrow.Arrow(2021, 1, 1, 14, 30, 21)
print(arrow_object)

This results in:

2021-01-01T14:30:21+00:00

Conversion Support with Arrow

Parsing a date and time from a string is a straightforward process with Arrow - you simply use the get() method, and supply it with a valid string format. Also, Arrow lets you effortlessly convert between its own implementation of datetime class and the built-in datetime object.

Convert String to Datetime with Arrow

If a string is already formatted in the ISO 8601 format (YYYY-MM-DDTHH:MM:SS.mmmmmm), it can be passed directly into the get() method:

import arrow
datetime = arrow.get('2021/03/30 12:05')
print(datetime)

This will print out the Arrow instance, which is Arrow's own implementation of the datetime interface:

<Arrow [2021-03-30T12:05:00+00:00]>

However, in practice, it is unlikely that we will be using correctly formatted strings, following the ISO specification.

Thankfully, we can still parse strings that don't adhere to conventions, by using correct Arrow format tokens. These are pre-defined and give Arrow the information needed to parse the string correctly:

import arrow
datetime = arrow.get('March 30 2021 12:05', 'MMMM DD YYYY HH:mm')
print(datetime)

Here, we've effectively told Arrow what the format is. It maps the supplied format tokens with the string we'd like to parse, and constructs an Arrow object based on that info. Running this results in:

<Arrow [2021-03-30T12:05:00+00:00]>

Convert Between Arrow and datetime Objects

So far, we've been working with Arrow instances. However, many applications and libraries explicitly require you to use a datetime object. Conversion between these two formats is crucial.

Let's take a look at the type() of our variable:

type(datetime)

Output:

arrow.arrow.Arrow

To convert this to a datetime instance, we simply extract the datetime field from the Arrow object:

datetime = datetime.datetime
print(datetime)

This results in a time-zone aware datetime instance:

datetime.datetime(2021, 3, 12, 12, 5, tzinfo=tzutc())

Even though we haven't specified the timezone when creating the original Arrow object, the datetime object has the tzinfo defaulted to UTC. We will refer back to this in the next section, when taking a more detailed look into handling timezones.

For now, we can confirm it is indeed a datetime object type:

type(now)

This results in;

datetime.datetime

Similarly enough, you can easily convert datetime objects into Arrow objects, using the fromdatetime() function:

datetime = datetime.datetime(2021, 1, 1, 0, 0)
arrow_object = arrow.Arrow.fromdatetime(datetime)
print(arrow_object)

This results in:

2021-01-01T00:00:00+00:00

Dealing with Timezones

One of the major issues with the datetime module is the way it handles timezones. It is considered timezone-naive, meaning it contains no timezone related data. Arrow on the other hand contains a tzinfo parameter for each instantiation, which you can set through the constructor, or through methods. The tzinfo defaults to UTC, regardless of the user's location.

In practical terms this means that with datetime, a user in Hong Kong would be working in local Hong Kong time whereas a user in UK would be working in local UK time - unless otherwise specified:

import datetime
datetime.datetime.now()

The output for the Hong Kong based user would be:

datetime.datetime(2021, 3, 31, 00, 35, 08, 114203)

Whereas someone based in the UK would see:

datetime.datetime(2021, 3, 30, 17, 35, 25, 119213)

Having a standard default timezone is increasingly important with the rise in remote working as well the globalization of projects. Having to explicitly set timezones for datetime objects gets stale fast. Arrow automates this process, in a single, unified, default timezone. You can set it to other timezones, of course, or even local ones.

Irrespective of the user's geographical location, the following lines of code will give the same output:

arrow.now()

Output:

<Arrow [2021-03-30T17:37:28.374335+01:00]>

We can confirm this corresponds to the UTC time:

arrow.utcnow()

Output:

<Arrow [2021-03-30T16:37:59.721766+00:00]>

We can set timezones on the Arrow instance simply by passing in the timezone string in its constructor:

arrow.now('US/Pacific)

This works alongside other parameters that we've used before. Setting a timezone while constructing an Arrow instance works both for converting strings and datetime objects and calling the constructor explicitly:

import arrow
import datetime as datetime


date = '2021.01.01 00:00'
datetime = datetime.datetime(2021, 1, 1, 0, 0)

arrow_1 = arrow.Arrow(2021, 1, 1, 0, 0, tzinfo='US/Pacific')
arrow_2 = arrow.get(date, tzinfo='US/Pacific')
arrow_3 = arrow.Arrow.fromdatetime(datetime, tzinfo='US/Pacific')

print(arrow_1)
print(arrow_2)
print(arrow_3)

Now, these Arrow instances will be set to the US/Pacific timezone, and set to the 1st of January, 2021:

2021-01-01T00:00:00-08:00
2021-01-01T00:00:00-08:00
2021-01-01T00:00:00-08:00

As you can see, the objects now have a -08:00 suffix, since the US/Pacific timezone is 8 hours behind UTC.

You can also extract the naive versions of these datetimes by calling their naive field:

print(arrow_1.naive)
print(arrow_2.naive)
print(arrow_3.naive)

This will not strip them off their timezone awareness, resulting in what you'd expect when you're working with naive datetime objects:

2021-01-01 00:00:00
2021-01-01 00:00:00
2021-01-01 00:00:00

Converting Between Timezones with Arrow

Another area worth looking at is how we can convert between different timezones with Arrow by using the to() method. Whilst Arrow is fully compatible with timezone modules, there is no need to import any additional modules for timezone conversion.

To start with we can get the current time and assign it to a variable:

utc=arrow.now()
print(utc)
<Arrow [2021-03-30T17:41:08.765166+01:00]>

Now, let's convert this object to another timezone:

utc.to('US/Pacific')

This results in:

<Arrow [2021-03-30T09:41:08.765166-07:00]>

Or the time in Hong Kong:

utc.to('Asia/Hong_Kong')
<Arrow [2021-03-30T09:41:08.765166-07:00]>

We can even specify the time difference with number of hours:

utc.to('-05:00')

This is a much more intuitive way to convert between timezones, especially if you don't have a list of the appropriate names handy:

<Arrow [2021-03-30T11:41:08.765166-05:00]>

Humanizing and Shifting Dates

Oftentimes, when dealing with time spans, we don't really need a date. When talking to colleagues, we say:

"I went shopping yesterday".

Not:

"I went shopping on 2021/12/03", which is the day before 2021/12/04", which is the day right now.

In a lot of cases, you might want to humanize dates, such as annotating when a notification arrived, an email was sent or when someone performed a certain logged action. Thankfully, Arrow allows us to really easily humanize any date via the humanize() function.

This function works wonders with the shift() function, which can shift the dates by 0...n days. You might want to make a system that notifies a person of when a certain date is coming up, in human-speak:

import arrow

now = arrow.now()
tomorrow = now.shift(days=1)
yesterday = now.shift(days=-1)
next_week = now.shift(days=7)
notification = now.shift(days=-2, hours=-5, minutes=-7)

print(now.humanize())
print(tomorrow.humanize())
print(yesterday.humanize())
print(next_week.humanize())
print(notification.humanize(granularity=['day', 'hour', 'minute']))

Here, we've created an Arrow object using the now() function. Then, we've created a series of objects by shifting the values up or down. The shift() function accepts arguments such as years, months, days, hours, minutes and seconds. We've created a tomorrow and yesterday object by shifting up and down by one day (not in-place), and an object for next_week and an arbitrary notification object that happened 2 days, 5 hours and 7 minutes ago.

Finally, when humanizing, you can specify the granularity, which lets Arrow know how detailed to be when it comes to reporting time in human-speak. By default, it'll set the granularity to day and/or week, depending on the time range. In our case, we've left the default settings on, except for the final object where we specifically want a bit of a finer granularity:

just now
in a day
a day ago
in a week
2 days 5 hours and 7 minutes ago

This is a very intuitive and human way to represent dates to your user - such as counting down days to an event, or counting days from an event.

Advantages of Using Arrow

The advantages of using Arrow could be summarized with the official statements from their documentation:

Sensible and human-friendly approach.

Supporting many common creation scenarios.

Help you work with dates and times with fewer imports and a lot less code.

In these, it appears to succeed, and Arrow overcomes the major issues with the datetime library. From what we have seen in our examples above Arrow is certainly an improvement in terms of:

  • Reducing the need for importing multiple modules
  • Working in just one data type (Arrow)
  • Being timezone-aware
  • Simplifying the creation of the most commonly used date and time functions
  • Easy conversion between various types
  • Helpful humanization functions
  • Easily shifting values into the past and future

Conclusion

In this guide, we've focused on some of the benefits of using the Arrow library to work with date and time in Python. It's inspired by Moment.js, and offers solutions to some of the known problems of the datetime library.

However, it is worth bearing in mind there are many other date and time modules available. It is also worth noting that being a built-in module gives datetime the advantage of needing users pro-actively looking to replace it.

It all comes down to how important the date and time element is to your project and your code.