5.11 Date and Time
Table of Contents
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:
| Class | What it represents | Example use |
|---|---|---|
date | Calendar date only (year, month, day) | User's birthday |
time | Time of day only (hour, minute, second) | Daily notification time |
datetime | Date and time together | When an order was created |
timedelta | Duration or difference between two dates or times | Token lifetime, subscription period |
timezone | Fixed offset time zone | Represent UTC or a fixed offset zone |
Basic imports:
from datetime import date, time, datetime, timedelta, timezoneCreating Dates, Times, and Datetimes
Creating `date` objects
You usually create a date by specifying year, month, and day:
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 20You can also get today's date:
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:
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:
from datetime import datetime
dt = datetime(2024, 5, 20, 14, 30, 15)
print(dt) # 2024-05-20 14:30:15Get the current local datetime (according to the server):
now_local = datetime.now()Get the current UTC datetime:
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:
- Naive: No time zone information
- Aware: Includes a time zone or offset from UTC
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:
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+).
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:
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.
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:00When building APIs, prefer ISO 8601 strings in UTC. Example 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:
| Code | Meaning | Example |
|---|---|---|
%Y | 4-digit year | 2024 |
%m | 2-digit month | 05 |
%d | 2-digit day | 20 |
%H | Hour (24h) | 14 |
%M | Minute | 30 |
%S | Second | 15 |
%z | UTC offset | +0000 |
%Z | Time zone name | UTC |
%a | Weekday (short) | Mon |
%A | Weekday (full) | Monday |
Example:
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 UTCBackend 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.
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:
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:
from datetime import timedelta
one_day = timedelta(days=1)
two_hours = timedelta(hours=2)
thirty_minutes = timedelta(minutes=30)Adding and subtracting
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:
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
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:
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:
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_atWorking 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
from datetime import datetime, timezone
dt = datetime(2024, 5, 20, 14, 30, 15, tzinfo=timezone.utc)
ts = dt.timestamp()
print(ts) # e.g. 1716215415.0For integer seconds:
ts_int = int(dt.timestamp())Timestamp to datetime
from datetime import datetime, timezone
ts = 1716215415
dt = datetime.fromtimestamp(ts, tz=timezone.utc)
print(dt) # 2024-05-20 14:30:15+00:00Backend 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:
- Database
- Backend code (Python)
- API responses and requests (JSON)
Common patterns
A common and safe pattern:
- Store datetimes in the database in UTC
- Use timezone-aware
datetimeobjects in Python withtimezone.utc - Serialize to ISO 8601 strings for JSON
Example FastAPI endpoint returning a timestamp:
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:
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 stringAvoiding common mistakes
Some common pitfalls:
- Mixing naive and aware datetimes
- Storing local time instead of UTC in the database
- Ignoring time zones when parsing user input
- Returning ambiguous date formats like
"05/06/2024"that can be interpreted differently by region
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:
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_atExample: Calculating age from birthdate
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 yearsExample: Scheduling a reminder 24 hours before an event
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:
- Use timezone aware
datetimeobjects withtimezone.utcin your backend. - Store all datetimes in the database in UTC.
- Use ISO 8601 strings for JSON APIs.
- Use
timedeltafor durations and arithmetic. - 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
KAHIBARO