---
title: Always Use Timezone-Aware Datetimes
impact: HIGH
impactDescription: Naive datetimes (without tzinfo) cause silent bugs when comparing times across timezones, leading to incorrect scheduling, expiry calculations, and audit logs.
tags: python, datetime, timezone, bugs, quality
---

## Always Use Timezone-Aware Datetimes

Python's `datetime` objects can be *naive* (no timezone) or *aware* (with timezone). Mixing naive and aware datetimes raises a `TypeError`, but naive datetimes used consistently silently produce wrong results when code runs in different timezones (CI server vs production, container vs host).

Always create timezone-aware datetimes by passing `tz` or `tzinfo`.

**Incorrect:**
```python
from datetime import datetime

# Naive datetimes — "now" means different things in Tokyo vs London
created_at = datetime.now()
expires_at = datetime.utcnow()  # UTC but naive — still dangerous

# Comparing naive datetimes assumes both are in same timezone (often wrong)
if datetime.now() > token_expiry:  # fails if token_expiry is aware
    raise TokenExpiredError()
```

**Correct:**
```python
from datetime import datetime, timezone

# Python 3.11+: use datetime.UTC constant
now = datetime.now(tz=timezone.utc)

# Python 3.9+: use zoneinfo for local zones
from zoneinfo import ZoneInfo

jst_now = datetime.now(tz=ZoneInfo("Asia/Tokyo"))
utc_now = datetime.now(tz=timezone.utc)

# django/pytz style (pre-3.9)
import pytz

utc_now = datetime.now(tz=pytz.utc)

# Always compare aware with aware
token_expiry: datetime  # must be aware
if datetime.now(tz=timezone.utc) > token_expiry:
    raise TokenExpiredError()
```

**Converting naive to aware (migration):**
```python
# Assume a legacy naive datetime is UTC — localize it explicitly
naive_dt = datetime(2024, 1, 15, 10, 30)
aware_dt = naive_dt.replace(tzinfo=timezone.utc)
```

**Tools:** Ruff `DTZ001`–`DTZ012` (flake8-datetimez), Pylint `W1502`, mypy with `--strict-optional`
