Intermediate
Dealing with dates and times in Python can feel like defusing a bomb. You need datetime, timedelta, pytz, dateutil, and a generous amount of patience just to answer a question like “what time will it be in Tokyo three days from now?” The standard library splits this work across multiple modules with inconsistent interfaces, and timezone handling alone is a common source of production bugs that are notoriously hard to reproduce.
The arrow library is a drop-in replacement that fixes these pain points without requiring you to learn an entirely new paradigm. It wraps Python’s built-in datetime objects in a cleaner API that is timezone-aware by default, handles parsing and formatting with one method each, and lets you shift dates forward and backward with human-readable keyword arguments. You install it with a single pip install arrow command and no other dependencies are needed.
In this tutorial we will cover everything you need to work confidently with arrow: creating and converting datetime objects, parsing and formatting dates, shifting dates with relative time, converting between timezones, humanizing time differences, and generating date ranges. By the end you will have a clear picture of when arrow saves you time and how to integrate it into real projects.
Python arrow Datetime: Quick Example
Before we dive in, here is a self-contained example showing the three things arrow does better than the standard library in just a handful of lines.
# quick_arrow.py
import arrow
# Get the current time in UTC
now = arrow.utcnow()
print("UTC now:", now)
# Convert to a specific timezone
sydney = now.to("Australia/Sydney")
print("Sydney time:", sydney.format("YYYY-MM-DD HH:mm:ss ZZZ"))
# Shift three days into the future
future = now.shift(days=3)
print("3 days from now:", future.humanize())
# Parse a date string
parsed = arrow.get("2025-12-31", "YYYY-MM-DD")
print("Parsed:", parsed)
print("Days until end of 2025:", (parsed - now).days)
Output:
UTC now: 2026-07-16T04:23:11.241819+00:00
Sydney time: 2026-07-16 14:23:11 AEST
3 days from now: in 3 days
Parsed: 2025-12-31T00:00:00+00:00
Days until end of 2025: -197
In six lines we accomplished what would normally require importing datetime, timedelta, and pytz and wiring them together manually. The to() method handles timezone conversion, shift() handles relative date arithmetic, and humanize() produces reader-friendly output. We will explore each of these capabilities in depth below.
What Is arrow and Why Use It?
The arrow library, created by Chris Smith, was designed with one goal: make working with dates and times in Python as painless as possible. It provides a single Arrow object type that always carries timezone information (defaulting to UTC), which eliminates the infamous “naive vs. aware datetime” class of bugs that plagues production applications.
Under the hood, every Arrow object wraps a standard Python datetime and the two are fully interoperable — you can convert between them at any point. This means you can adopt arrow incrementally in an existing codebase without rewriting everything at once.
Here is a direct comparison of how common tasks look with the standard library versus arrow:
| Task | Standard Library | arrow |
|---|---|---|
| Current UTC time | datetime.utcnow() (naive) | arrow.utcnow() (tz-aware) |
| Add 3 days | dt + timedelta(days=3) | dt.shift(days=3) |
| Timezone conversion | dt.astimezone(pytz.timezone(...)) | dt.to("US/Eastern") |
| Parse a string | datetime.strptime(s, "%Y-%m-%d") | arrow.get(s, "YYYY-MM-DD") |
| Format a date | dt.strftime("%B %d, %Y") | dt.format("MMMM DD, YYYY") |
| Human-readable diff | Write it yourself | dt.humanize() |
| Date range | Write it yourself | arrow.Arrow.range(...) |
The pattern is consistent: arrow replaces strftime/strptime format codes with the more readable YYYY-MM-DD token style (shared with Moment.js and many other libraries), bundles timezone support into the core object, and adds missing utilities like humanize() and range() that you would otherwise have to write from scratch. Let us look at each capability in detail.
Installing arrow
Arrow is available on PyPI and requires Python 3.6 or later. There are no heavy dependencies — it uses Python-dateutil under the hood, which is installed automatically.
# terminal
pip install arrow
Output:
Successfully installed arrow-1.3.0 python-dateutil-2.9.0
Verify the installation worked by running a quick import check:
# verify_install.py
import arrow
print(arrow.__version__)
print(arrow.now())
Output:
1.3.0
2026-07-16T14:23:11.241819+10:00
Creating Arrow Objects
There are several ways to create an Arrow object, depending on where your date data is coming from. The most common entry points are arrow.now(), arrow.utcnow(), arrow.get(), and arrow.Arrow().
now() and utcnow()
Use arrow.now() to get the current local time (it detects your system timezone automatically) and arrow.utcnow() to get the current time in UTC. Both return timezone-aware objects, unlike Python’s built-in datetime.now() which returns a naive (timezone-free) object.
# creating_objects.py
import arrow
# Local time (uses your system timezone)
local = arrow.now()
print("Local:", local)
print("Timezone:", local.tzname())
# UTC time
utc = arrow.utcnow()
print("UTC:", utc)
print("Timezone:", utc.tzname())
# Specify a timezone explicitly
tokyo = arrow.now("Asia/Tokyo")
print("Tokyo:", tokyo)
Output:
Local: 2026-07-16T14:23:11.241819+10:00
Timezone: AEST
UTC: 2026-07-16T04:23:11.241819+00:00
Timezone: UTC
Tokyo: 2026-07-16T13:23:11.241819+09:00
Constructing from Values or Strings
The arrow.get() function is the general-purpose factory. It accepts integers (Unix timestamps), datetime objects, or date strings with a matching format token. This flexibility means you rarely need more than one function name to bring data into arrow’s type system.
# get_examples.py
import arrow
import datetime
# From a Unix timestamp
from_ts = arrow.get(1736000000)
print("From timestamp:", from_ts)
# From a Python datetime (naive -- arrow assumes UTC)
native_dt = datetime.datetime(2026, 6, 15, 12, 0, 0)
from_dt = arrow.get(native_dt)
print("From datetime:", from_dt)
# From a date string with explicit format
from_str = arrow.get("16/07/2026 09:30", "DD/MM/YYYY HH:mm")
print("From string:", from_str)
# From ISO 8601 (arrow detects this automatically)
from_iso = arrow.get("2026-07-16T09:30:00+05:30")
print("From ISO:", from_iso)
# Explicit construction (year, month, day, ...)
explicit = arrow.Arrow(2026, 12, 31, 23, 59, 59)
print("Explicit:", explicit)
Output:
From timestamp: 2025-01-04T19:33:20+00:00
From datetime: 2026-06-15T12:00:00+00:00
From string: 2026-07-16T09:30:00+00:00
From ISO: 2026-07-16T09:30:00+05:30
Explicit: 2026-12-31T23:59:59+00:00
Notice that when you pass a naive datetime to arrow.get(), arrow assumes UTC. This is a deliberate design choice — it is always safer to be explicit, and UTC is the most common default in server-side code. If your naive datetime is actually in a local timezone, convert it first with arrow.get(dt, tzinfo=local_tz).
Formatting and Parsing Dates
Arrow uses the same token system for both formatting and parsing, which means you only need to learn one set of tokens. The tokens are easier to read than Python’s %Y-%m-%d strftime codes because they use the full unit name instead of a percent-letter code.
Common Format Tokens
| Token | Output | Description |
|---|---|---|
YYYY | 2026 | 4-digit year |
YY | 26 | 2-digit year |
MM | 07 | Zero-padded month |
MMMM | July | Full month name |
MMM | Jul | Short month name |
DD | 16 | Zero-padded day |
HH | 14 | 24-hour hour |
hh | 02 | 12-hour hour |
mm | 23 | Minutes |
ss | 11 | Seconds |
ZZZ | AEST | Timezone abbreviation |
Z | +10:00 | UTC offset |
# formatting.py
import arrow
dt = arrow.Arrow(2026, 7, 16, 14, 23, 11, tzinfo="Australia/Sydney")
# Format with various patterns
print(dt.format("YYYY-MM-DD"))
print(dt.format("DD MMMM YYYY"))
print(dt.format("MMMM D, YYYY [at] h:mm A"))
print(dt.format("ddd, DD MMM YYYY HH:mm:ss Z"))
print(dt.isoformat())
Output:
2026-07-16
16 July 2026
July 16, 2026 at 2:23 PM
Thu, 16 Jul 2026 14:23:11 +10:00
2026-07-16T14:23:11+10:00
The square brackets in MMMM D, YYYY [at] h:mm A are an escape mechanism — any text inside [...] is treated as a literal string rather than a format token, which is how we include the word “at” without it being interpreted as a token.
Parsing Date Strings
Parsing uses the same tokens in the format argument of arrow.get(). If your input is ISO 8601, you do not need a format argument at all — arrow detects it automatically.
# parsing.py
import arrow
# Automatic ISO 8601 detection
iso = arrow.get("2026-07-16T14:23:11+10:00")
print("ISO:", iso)
# Custom format
custom = arrow.get("July 16, 2026", "MMMM D, YYYY")
print("Custom:", custom)
# European date format
european = arrow.get("16/07/2026", "DD/MM/YYYY")
print("European:", european)
# With timezone in the string
with_tz = arrow.get("2026-07-16 14:23:11 Australia/Sydney", "YYYY-MM-DD HH:mm:ss ZZZ")
print("With TZ:", with_tz)
# Multiple possible formats (arrow tries each)
for date_str in ["2026-07-16", "07/16/2026", "July 16 2026"]:
formats = ["YYYY-MM-DD", "MM/DD/YYYY", "MMMM D YYYY"]
for fmt in formats:
try:
parsed = arrow.get(date_str, fmt)
print(f"Parsed '{date_str}' as {parsed.date()}")
break
except Exception:
continue
Output:
ISO: 2026-07-16T14:23:11+10:00
Custom: 2026-07-16T00:00:00+00:00
European: 2026-07-16T00:00:00+00:00
With TZ: 2026-07-16T14:23:11+10:00
Parsed '2026-07-16' as 2026-07-16
Parsed '07/16/2026' as 2026-07-16
Parsed 'July 16 2026' as 2026-07-16
Shifting and Manipulating Dates
The shift() method is arguably the most useful feature in arrow. Instead of importing timedelta and doing manual arithmetic, you pass keyword arguments for the time units you want to add or subtract. Negative values shift backward in time.
# shifting.py
import arrow
now = arrow.Arrow(2026, 7, 16, 14, 0, 0, tzinfo="UTC")
print("Base:", now)
# Shift forward
print("+3 days:", now.shift(days=3))
print("+2 weeks:", now.shift(weeks=2))
print("+6 months:", now.shift(months=6))
print("+1 year:", now.shift(years=1))
# Shift backward
print("-30 minutes:", now.shift(minutes=-30))
print("-1 month:", now.shift(months=-1))
# Combine multiple units
print("+1 year 3 months 5 days:", now.shift(years=1, months=3, days=5))
# Chaining is also possible
result = now.shift(months=1).shift(days=-5).shift(hours=8)
print("Chained:", result)
Output:
Base: 2026-07-16T14:00:00+00:00
+3 days: 2026-07-19T14:00:00+00:00
+2 weeks: 2026-07-30T14:00:00+00:00
+6 months: 2027-01-16T14:00:00+00:00
+1 year: 2027-07-16T14:00:00+00:00
-30 minutes: 2026-07-16T13:30:00+00:00
-1 month: 2026-06-16T14:00:00+00:00
+1 year 3 months 5 days: 2027-10-21T14:00:00+00:00
Chained: 2026-08-11T22:00:00+00:00
One thing to note about shift(months=1): arrow handles month boundaries correctly. If you are on January 31 and shift by one month, you get February 28 (or 29 in a leap year) rather than an error. This is a common edge case that catches developers off guard when using raw timedelta arithmetic, which only works in days and cannot express “one calendar month.”
Floor, Ceil, and Span
Arrow provides floor() and ceil() methods to snap a datetime to the start or end of a time period (day, week, month, year, etc.). The span() method returns both the floor and ceil as a tuple, which is useful for building date range queries.
# floor_ceil.py
import arrow
dt = arrow.Arrow(2026, 7, 16, 14, 23, 45, tzinfo="UTC")
print("Original:", dt)
# Floor -- snap to start of period
print("Start of day:", dt.floor("day"))
print("Start of month:", dt.floor("month"))
print("Start of year:", dt.floor("year"))
print("Start of hour:", dt.floor("hour"))
# Ceil -- snap to end of period
print("End of day:", dt.ceil("day"))
print("End of month:", dt.ceil("month"))
# Span -- returns (floor, ceil) tuple
start, end = dt.span("month")
print(f"This month: {start} to {end}")
start, end = dt.span("week")
print(f"This week: {start} to {end}")
Output:
Original: 2026-07-16T14:23:45+00:00
Start of day: 2026-07-16T00:00:00+00:00
Start of month: 2026-07-01T00:00:00+00:00
Start of year: 2026-01-01T00:00:00+00:00
Start of hour: 2026-07-16T14:00:00+00:00
End of day: 2026-07-16T23:59:59.999999+00:00
End of month: 2026-07-31T23:59:59.999999+00:00
This month: 2026-07-01T00:00:00+00:00 to 2026-07-31T23:59:59.999999+00:00
This week: 2026-07-13T00:00:00+00:00 to 2026-07-19T23:59:59.999999+00:00
These methods are invaluable when writing database queries. A pattern like start, end = arrow.now().span("month") gives you the exact UTC timestamps for “this calendar month” without manually calculating month lengths or worrying about leap years.
Timezone Handling
Timezone conversions in arrow are a single method call. The to() method accepts any IANA timezone name (the same names used by pytz) and returns a new Arrow object representing the same moment in the target timezone.
# timezones.py
import arrow
# Start in UTC
utc = arrow.utcnow()
print("UTC: ", utc.format("YYYY-MM-DD HH:mm:ss ZZZ"))
# Convert to multiple zones
zones = [
"US/Eastern",
"US/Pacific",
"Europe/London",
"Europe/Paris",
"Asia/Kolkata",
"Asia/Tokyo",
"Australia/Sydney",
]
for zone in zones:
local = utc.to(zone)
print(f"{zone:<20} {local.format('YYYY-MM-DD HH:mm:ss ZZZ')}")
Output:
UTC: 2026-07-16T04:23:11+00:00 UTC
US/Eastern 2026-07-16 00:23:11 EDT
US/Pacific 2026-07-15 21:23:11 PDT
Europe/London 2026-07-16 05:23:11 BST
Europe/Paris 2026-07-16 06:23:11 CEST
Asia/Kolkata 2026-07-16 09:53:11 IST
Asia/Tokyo 2026-07-16 13:23:11 JST
Australia/Sydney 2026-07-16 14:23:11 AEST
Arrow also handles Daylight Saving Time transitions automatically. If you store a UTC timestamp and convert it later, arrow will use the correct offset for that specific date -- so a timestamp from January will correctly show EST (-5) while a timestamp from July will show EDT (-4) for the US/Eastern zone. You never have to think about DST offset tables.
Humanizing Dates
The humanize() method converts a time difference into plain language. It is the feature most requested by developers who use dates in user-facing applications -- displaying "3 hours ago" or "in 2 days" instead of a raw timestamp is almost always better for readability.
# humanize.py
import arrow
now = arrow.utcnow()
# Relative to now (default)
moments = [
now.shift(seconds=-30),
now.shift(minutes=-5),
now.shift(hours=-2),
now.shift(days=-1),
now.shift(weeks=-2),
now.shift(months=-3),
now.shift(years=-1),
now.shift(minutes=5),
now.shift(hours=3),
now.shift(days=2),
]
for moment in moments:
print(moment.humanize())
print()
# Humanize relative to a specific reference time
reference = arrow.Arrow(2026, 1, 1, tzinfo="UTC")
print(now.humanize(reference)) # relative to Jan 1
print(reference.humanize(now)) # relative to now
Output:
30 seconds ago
5 minutes ago
2 hours ago
a day ago
2 weeks ago
3 months ago
a year ago
in 5 minutes
in 3 hours
in 2 days
in 6 months
6 months ago
You can also request a specific granularity level using the granularity parameter, which accepts a list of units. For example, humanize(granularity=["hour", "minute"]) will produce output like "2 hours and 15 minutes ago" instead of the default single-unit output.
Generating Date Ranges
Arrow's Arrow.range() class method generates a sequence of evenly spaced dates between a start and end point. This is useful for generating report periods, calendar views, or any situation where you need to iterate over a regular sequence of dates.
# date_ranges.py
import arrow
start = arrow.Arrow(2026, 7, 1, tzinfo="UTC")
end = arrow.Arrow(2026, 7, 10, tzinfo="UTC")
# Daily range
print("Daily range (July 1-10):")
for dt in arrow.Arrow.range("day", start, end):
print(" ", dt.format("YYYY-MM-DD dddd"))
print()
# Weekly range for a quarter
q3_start = arrow.Arrow(2026, 7, 1, tzinfo="UTC")
q3_end = arrow.Arrow(2026, 9, 30, tzinfo="UTC")
print("Weekly intervals in Q3 2026:")
for week_start in arrow.Arrow.range("week", q3_start, q3_end):
week_end = week_start.shift(days=6)
print(f" {week_start.format('MMM D')} - {week_end.format('MMM D')}")
Output:
Daily range (July 1-10):
2026-07-01 Wednesday
2026-07-02 Thursday
2026-07-03 Friday
2026-07-04 Saturday
2026-07-05 Sunday
2026-07-06 Monday
2026-07-07 Tuesday
2026-07-08 Wednesday
2026-07-09 Thursday
2026-07-10 Friday
Weekly intervals in Q3 2026:
Jul 1 - Jul 7
Jul 8 - Jul 14
Jul 15 - Jul 21
Jul 22 - Jul 28
Jul 29 - Aug 4
Aug 5 - Aug 11
Aug 12 - Aug 18
Aug 19 - Aug 25
Aug 26 - Sep 1
Sep 2 - Sep 8
Sep 9 - Sep 15
Sep 16 - Sep 22
Sep 23 - Sep 29
The first argument to range() can be any of: "year", "month", "week", "day", "hour", "minute", or "second". Arrow also provides Arrow.span_range() if you need both the start and end of each interval returned as a tuple instead of just the start.
Interoperability with the Standard Library
Because arrow wraps Python's standard datetime, converting between the two is trivial. You will often need this when passing dates to libraries that do not know about arrow (like most database drivers, Django ORM fields, or pandas).
# interop.py
import arrow
import datetime
# Arrow to standard datetime
a = arrow.Arrow(2026, 7, 16, 14, 0, 0, tzinfo="UTC")
native = a.datetime # timezone-aware datetime
naive = a.naive # timezone-naive datetime (UTC values, no tzinfo)
date_only = a.date() # datetime.date object
ts = a.timestamp() # Unix timestamp (float)
print("Arrow: ", a)
print("datetime: ", native)
print("naive: ", naive)
print("date: ", date_only)
print("timestamp:", ts)
# Standard datetime to arrow
now_dt = datetime.datetime.now(datetime.timezone.utc)
back_to_arrow = arrow.get(now_dt)
print("Back to arrow:", back_to_arrow)
# Arithmetic with timedelta still works
delta = datetime.timedelta(hours=5)
shifted = a + delta
print("Arrow + timedelta:", shifted)
Output:
Arrow: 2026-07-16T14:00:00+00:00
datetime: 2026-07-16 14:00:00+00:00
naive: 2026-07-16 14:00:00
date: 2026-07-16
timestamp: 1752674400.0
Back to arrow: 2026-07-16T04:23:11.241819+00:00
Arrow + timedelta: 2026-07-16T19:00:00+00:00
Real-Life Example: Project Deadline Tracker
Let us build a small command-line deadline tracker that stores project milestones and reports their status in human-friendly language. This pulls together creation, shifting, timezone conversion, humanizing, and formatting into a single practical application.
# deadline_tracker.py
import arrow
# Milestones stored as dicts with ISO 8601 deadline strings
milestones = [
{"name": "API spec review", "deadline": "2026-07-10T17:00:00+10:00"},
{"name": "Backend integration", "deadline": "2026-07-25T17:00:00+10:00"},
{"name": "Frontend handoff", "deadline": "2026-08-01T17:00:00+10:00"},
{"name": "QA testing complete", "deadline": "2026-08-15T17:00:00+10:00"},
{"name": "Production deployment", "deadline": "2026-08-20T17:00:00+10:00"},
]
def status_label(deadline_arrow, now):
"""Return a status label based on how far away the deadline is."""
diff_days = (deadline_arrow - now).days
if diff_days < 0:
return "OVERDUE"
elif diff_days <= 3:
return "URGENT"
elif diff_days <= 7:
return "THIS WEEK"
else:
return "ON TRACK"
def print_milestone_report(milestones, viewer_tz="Australia/Sydney"):
now = arrow.utcnow()
print(f"Project Milestone Report")
print(f"Generated: {now.to(viewer_tz).format('MMMM D, YYYY [at] h:mm A ZZZ')}")
print(f"{'=' * 60}")
for m in milestones:
deadline = arrow.get(m["deadline"])
local_deadline = deadline.to(viewer_tz)
label = status_label(deadline, now)
print(f"\n{m['name']}")
print(f" Deadline: {local_deadline.format('ddd, MMMM D YYYY [at] h:mm A ZZZ')}")
print(f" Status: [{label}] {deadline.humanize()}")
# If overdue, show how long it has been
if label == "OVERDUE":
overdue_hours = abs((deadline - now).total_seconds()) / 3600
print(f" Overdue by: {overdue_hours:.1f} hours")
# Summary
total = len(milestones)
overdue = sum(1 for m in milestones if (arrow.get(m["deadline"]) - now).days < 0)
print(f"\n{'=' * 60}")
print(f"Summary: {total - overdue}/{total} milestones on track, {overdue} overdue")
print_milestone_report(milestones)
Output:
Project Milestone Report
Generated: July 16, 2026 at 2:23 PM AEST
============================================================
API spec review
Deadline: Thu, July 10 2026 at 5:00 PM AEST
Status: [OVERDUE] 6 days ago
Overdue by: 141.4 hours
Backend integration
Deadline: Sat, July 25 2026 at 5:00 PM AEST
Status: [ON TRACK] in 9 days
Frontend handoff
Deadline: Sat, August 01 2026 at 5:00 PM AEST
Status: [ON TRACK] in 16 days
QA testing complete
Deadline: Sat, August 15 2026 at 5:00 PM AEST
Status: [ON TRACK] in a month
Production deployment
Deadline: Thu, August 20 2026 at 5:00 PM AEST
Status: [ON TRACK] in a month
============================================================
Summary: 4/5 milestones on track, 1 overdue
This script reads deadline strings directly from your data source (API response, config file, database), converts them to the viewer's local timezone for display, uses humanize() for the status column, and calculates numeric differences for the overdue alert. Extending it to load milestones from a JSON file or database query would take fewer than 10 additional lines.
Frequently Asked Questions
What is the difference between arrow and Pendulum?
Both arrow and Pendulum aim to improve Python's datetime API, but they make different trade-offs. Arrow is lighter-weight and has been around since 2013 with a simple API focused on the most common tasks. Pendulum is more opinionated -- it is a full drop-in replacement for datetime (meaning Pendulum objects behave like datetime objects at the type level), provides duration arithmetic with DST handling built in, and produces more precise human-readable intervals. For most applications, arrow is sufficient and easier to integrate. If you need Pendulum's stricter type compatibility or precise DST-aware duration math, use Pendulum instead.
How do I convert a naive datetime to an arrow object with the correct timezone?
If you have a naive datetime that is in a known timezone (not UTC), do not pass it directly to arrow.get() because arrow will assume UTC. Instead, use arrow.get(dt, tzinfo) where tzinfo is a timezone string or pytz object: arrow.get(naive_dt, "America/New_York"). This tells arrow "this naive datetime represents a moment in New York time" and produces a properly localized Arrow object.
How should I store arrow datetimes in a database?
Always store dates in UTC. Use a.datetime to get a timezone-aware datetime and pass that to your ORM or database driver. When reading back, wrap the retrieved value in arrow.get(db_value) and then call .to("your/timezone") for display. This pattern is timezone-safe and avoids the common bug of storing local times that become ambiguous during DST changes.
Can I compare two Arrow objects directly?
Yes. Arrow objects support all standard comparison operators: ==, !=, <, >, <=, >=. Comparisons are timezone-aware -- two Arrow objects representing the same moment in different timezones will compare as equal. You can also subtract two Arrow objects to get a timedelta: (deadline - now).days gives you the integer number of days between them.
How do I serialize an Arrow object to JSON?
Arrow objects are not directly JSON-serializable, but converting them is one line. Use a.isoformat() to get an ISO 8601 string for storage or API responses. When reading back from JSON, use arrow.get(json_string) -- arrow automatically parses ISO 8601 strings without needing a format argument. For Django REST Framework, Pydantic, or similar frameworks, add a custom serializer that calls .isoformat() on Arrow objects.
Does arrow support localized (non-English) output?
Yes. The humanize() method accepts a locale parameter: arrow.now().shift(days=-3).humanize(locale="fr") returns "il y a 3 jours". Arrow ships with locale support for over 50 languages. For formatting month and day names in a specific locale (like "Juli" in German instead of "July"), combine arrow's formatting with Python's locale module -- arrow itself uses English names in format() regardless of locale.
Conclusion
The arrow library removes most of the friction from working with dates and times in Python. We covered the full workflow: creating Arrow objects from timestamps, strings, and native datetimes; formatting and parsing with readable token patterns; shifting dates by months and weeks without manual timedelta math; converting between timezones in a single method call; producing human-readable time descriptions with humanize(); and generating evenly spaced date ranges. The deadline tracker project tied all of these together in a pattern you can adapt to any application that works with time-sensitive data.
The best next step is to replace one place in your existing code where you import datetime and pytz together -- that is arrow's strongest use case. The official documentation at arrow.readthedocs.io covers additional features including custom factory classes, token extension, and the full list of supported locales.