KAHIBARO
Discord Login Register

5.11 Date and Time

Working with Date and Time in Python

Date and time handling is a common backend task, for example when recording when a user registered, when an order was created, or when a token expires. Python provides powerful tools for this, but there are also many common mistakes.

This chapter focuses on how to use Python's date and time features correctly in backend applications, especially with time zones and serialization for APIs.


The `datetime` Module Overview

Python's built in datetime module is your main tool for working with dates and times.

It provides several key classes:

ClassWhat it representsExample use
dateCalendar date only (year, month, day)User's birthday
timeTime of day only (hour, minute, second)Daily notification time
datetimeDate and time togetherWhen an order was created
timedeltaDuration or difference between two dates or timesToken lifetime, subscription period
timezoneFixed offset time zoneRepresent UTC or a fixed offset zone

Basic imports:

python
from datetime import date, time, datetime, timedelta, timezone

Creating Dates, Times, and Datetimes

Creating `date` objects

You usually create a date by specifying year, month, and day:

python
from datetime import date
d = date(2024, 5, 20)   # year, month, day
print(d)                # 2024-05-20
print(d.year, d.month, d.day)  # 2024 5 20

You can also get today's date:

python
today = date.today()

Backend example: store a user's birthday as a date, because the time of day is not important.

Creating `time` objects

A time holds only time of day, no date:

python
from datetime import time
t = time(14, 30, 15)  # hour, minute, second
print(t)              # 14:30:15

time is less commonly stored in backends than datetime, but it can be useful for things like "send a daily email at 09:00".

Creating `datetime` objects

datetime combines date and time:

python
from datetime import datetime
dt = datetime(2024, 5, 20, 14, 30, 15)
print(dt)  # 2024-05-20 14:30:15

Get the current local datetime (according to the server):

python
now_local = datetime.now()

Get the current UTC datetime:

python
from datetime import timezone
now_utc = datetime.now(timezone.utc)

Backend example: store created_at and updated_at timestamps for database records as datetime values in UTC.


Naive vs Aware Datetimes and Time Zones

Time zones are a common source of bugs in backend systems, especially when servers, users, and databases are in different locations.

Naive vs aware

Python datetime objects are either:

python
from datetime import datetime, timezone
naive = datetime.now()               # naive, no tzinfo
aware = datetime.now(timezone.utc)   # aware, tzinfo=UTC
print(naive.tzinfo)  # None
print(aware.tzinfo)  # UTC

Always store and process server-side datetimes in UTC using timezone aware datetime objects.

If you forget this rule, comparisons and calculations across time zones can become wrong, especially when daylight saving time changes.

Representing UTC correctly

To get a correct UTC timestamp:

python
from datetime import datetime, timezone
now_utc = datetime.now(timezone.utc)

Avoid using datetime.utcnow() in new code because it returns a naive datetime with no timezone, even though it represents UTC.

Converting between time zones

The standard library datetime.timezone only supports fixed offsets, not full region time zones like "Europe/Berlin". For those, use zoneinfo (Python 3.9+).

python
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
utc_now = datetime.now(timezone.utc)           # aware UTC datetime
# Convert to a user's local time zone
user_tz = ZoneInfo("Europe/Berlin")
user_time = utc_now.astimezone(user_tz)
print(utc_now)   # 2024-05-20 13:00:00+00:00
print(user_time) # 2024-05-20 15:00:00+02:00 (example)

Backend example: store login timestamps in UTC. When showing them in the UI, convert to the user's selected time zone.

Attaching a time zone

If you know a naive datetime is in UTC, you can "attach" UTC:

python
from datetime import datetime, timezone
naive_utc = datetime.utcnow()  # naive, but logically UTC
aware_utc = naive_utc.replace(tzinfo=timezone.utc)

Be careful: replace(tzinfo=...) does not convert the time, it only attaches a time zone. Use astimezone when you want conversion.


Formatting and Parsing Dates and Times

Backend applications need to serialize datetimes to strings for JSON, logs, emails, and parse them back from incoming data.

ISO 8601 format

The most common format is ISO 8601. Python can produce and parse it easily.

python
from datetime import datetime, timezone
now = datetime(2024, 5, 20, 14, 30, 15, tzinfo=timezone.utc)
iso_str = now.isoformat()
print(iso_str)  # 2024-05-20T14:30:15+00:00

When building APIs, prefer ISO 8601 strings in UTC. Example JSON:

json
{
  "id": 1,
  "created_at": "2024-05-20T14:30:15Z",
  "expires_at": "2024-05-21T14:30:15Z"
}

Z is a shorthand for +00:00 (UTC).

Custom formatting with `strftime`

Use strftime to format datetimes as strings using format codes.

Common codes:

CodeMeaningExample
%Y4-digit year2024
%m2-digit month05
%d2-digit day20
%HHour (24h)14
%MMinute30
%SSecond15
%zUTC offset+0000
%ZTime zone nameUTC
%aWeekday (short)Mon
%AWeekday (full)Monday

Example:

python
from datetime import datetime, timezone
dt = datetime(2024, 5, 20, 14, 30, 15, tzinfo=timezone.utc)
formatted = dt.strftime("%Y-%m-%d %H:%M:%S %Z")
print(formatted)  # 2024-05-20 14:30:15 UTC

Backend example: create a human readable timestamp for logs or emails, but still use ISO 8601 for APIs.

Parsing strings with `strptime`

Use strptime to parse a string into a datetime, using the same format codes.

python
from datetime import datetime
s = "2024-05-20 14:30:15"
dt = datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
print(dt)  # 2024-05-20 14:30:15

If the string contains a time zone offset, include %z:

python
s = "2024-05-20T14:30:15+00:00"
dt = datetime.strptime(s, "%Y-%m-%dT%H:%M:%S%z")
print(dt.tzinfo)  # UTC offset

Backend example: parse a date string from a user form, such as 2024-05-20, into a date.


Doing Date and Time Arithmetic

You often need to calculate future or past times, durations, or differences between timestamps. For this, use timedelta.

Creating and using `timedelta`

timedelta represents a duration:

python
from datetime import timedelta
one_day = timedelta(days=1)
two_hours = timedelta(hours=2)
thirty_minutes = timedelta(minutes=30)
Adding and subtracting
python
from datetime import datetime, timezone, timedelta
now = datetime.now(timezone.utc)
tomorrow = now + timedelta(days=1)
yesterday = now - timedelta(days=1)

Backend example: generate a token that expires in 15 minutes:

python
from datetime import datetime, timezone, timedelta
def generate_expiry(minutes=15):
    return datetime.now(timezone.utc) + timedelta(minutes=minutes)
expires_at = generate_expiry()
Difference between two datetimes
python
from datetime import datetime, timezone
start = datetime(2024, 5, 20, 10, 0, tzinfo=timezone.utc)
end = datetime(2024, 5, 20, 12, 30, tzinfo=timezone.utc)
delta = end - start
print(delta)           # 2:30:00
print(delta.total_seconds())  # 9000.0

total_seconds() is very useful when you need to convert durations to seconds, for example for timeout values.

Comparing datetimes

You can compare datetimes using <, <=, >, >=, ==, !=.

Only compare datetimes that are either both naive, or both aware with compatible time zones. Never mix naive and aware datetimes.

Incorrect comparison raises a TypeError:

python
from datetime import datetime, timezone
naive = datetime.now()
aware = datetime.now(timezone.utc)
# This will raise TypeError
# print(naive < aware)

Backend example: check if a token is expired:

python
from datetime import datetime, timezone
def is_expired(expires_at: datetime) -> bool:
    # assume expires_at is an aware UTC datetime
    return datetime.now(timezone.utc) >= expires_at

Working with Unix Timestamps

A Unix timestamp is the number of seconds since the Unix epoch, which is defined as 1970-01-01 00:00:00 UTC.

Timestamps are common in databases, caches, and some APIs.

Datetime to timestamp

python
from datetime import datetime, timezone
dt = datetime(2024, 5, 20, 14, 30, 15, tzinfo=timezone.utc)
ts = dt.timestamp()
print(ts)  # e.g. 1716215415.0

For integer seconds:

python
ts_int = int(dt.timestamp())

Timestamp to datetime

python
from datetime import datetime, timezone
ts = 1716215415
dt = datetime.fromtimestamp(ts, tz=timezone.utc)
print(dt)  # 2024-05-20 14:30:15+00:00

Backend example: store expiry times in Redis as Unix timestamps, and convert to datetime when needed.


Date and Time in Backend APIs

When building backend services, you must decide how to represent dates and times between:

Common patterns

A common and safe pattern:

  1. Store datetimes in the database in UTC
  2. Use timezone-aware datetime objects in Python with timezone.utc
  3. Serialize to ISO 8601 strings for JSON

Example FastAPI endpoint returning a timestamp:

python
from datetime import datetime, timezone, timedelta
from fastapi import FastAPI
app = FastAPI()
@app.get("/token")
def get_token():
    now = datetime.now(timezone.utc)
    expires_at = now + timedelta(minutes=15)
    return {
        "token": "example",
        "issued_at": now.isoformat(),
        "expires_at": expires_at.isoformat(),
    }

Example of reading a datetime from a request payload with Pydantic:

python
from datetime import datetime
from pydantic import BaseModel
class Event(BaseModel):
    name: str
    # Pydantic can parse ISO 8601 strings into datetime automatically
    start_time: datetime
# In a FastAPI endpoint, Event will parse the incoming JSON string

Avoiding common mistakes

Some common pitfalls:

Preferred JSON datetime format in backends:

Use ISO 8601 UTC strings such as "2024-05-20T14:30:15Z" for API request and response data.


Practical Examples for Backend Use Cases

Example: Generating expiration dates

Token that expires in 1 hour, stored in database:

python
from datetime import datetime, timezone, timedelta
def generate_token_data():
    issued_at = datetime.now(timezone.utc)
    expires_at = issued_at + timedelta(hours=1)
    return issued_at, expires_at

Example: Calculating age from birthdate

python
from datetime import date
def calculate_age(birthdate: date, today: date | None = None) -> int:
    if today is None:
        today = date.today()
    years = today.year - birthdate.year
    # Adjust if birthday has not occurred yet this year
    if (today.month, today.day) < (birthdate.month, birthdate.day):
        years -= 1
    return years

Example: Scheduling a reminder 24 hours before an event

python
from datetime import datetime, timezone, timedelta
def reminder_time(event_start: datetime) -> datetime:
    # assume event_start is aware UTC datetime
    return event_start - timedelta(hours=24)
event_start = datetime(2024, 6, 1, 10, 0, tzinfo=timezone.utc)
print(reminder_time(event_start))

Summary

Working correctly with date and time is critical in backend development. The most important practical rules:

  1. Use timezone aware datetime objects with timezone.utc in your backend.
  2. Store all datetimes in the database in UTC.
  3. Use ISO 8601 strings for JSON APIs.
  4. Use timedelta for durations and arithmetic.
  5. Never mix naive and aware datetimes.

If you follow these rules and use the datetime module correctly, your backend will handle time reliably, even across different time zones and daylight saving changes.

Views: 6

Comments

Please login to add a comment.

Don't have an account? Register now!