datetime.astimezone

The converter. It moves the value to UTC using its own offset, then lets tz.fromutc() compute the new local fields — so the result compares equal to the input. On a naive value, or with no tz, the machine's local zone gets involved.

datetime methodPython 3.3+ (tz optional), naive input 3.6+Live demo
Common call
dt.astimezone(UTC)
Returns
the same instant expressed in UTC
Replaces
Adding and subtracting offsets by hand
Watch out
Naive dt is assumed to be LOCAL time, not UTC
dt.astimezone(tztz — Target zone: UTC, timezone(offset), ZoneInfo(...). None means the system local zone (a fixed-offset timezone for that moment).type: tzinfo | None · default: None=None)
→ datetime

Demo

Live evaluation
One instant, shown at another offset. The equality check proves nothing moved.
Try:
Inputs
whenstran aware ISO 8601 timestamp
hoursinttarget offset in hours
Code
from datetime import datetime, timedelta, timezone
dt = datetime.fromisoformat('2026-09-29T20:00Z')
if dt.tzinfo is None:
    raise ValueError('give the timestamp an offset, e.g. Z or +02:00')
local = dt.astimezone(timezone(timedelta(hours=9)))
(local.isoformat(), local == dt)
Result
('2026-09-30T05:00:00+09:00', True)

Converting can change the date: 20:00 UTC is already 05:00 the next day at +09:00. The == check is True every time because aware datetimes compare by instant. Naive input is stopped by the template: CPython would treat it as the machine's local time, which a demo cannot know.

Parameters

NameTypeRequiredDescription
tztzinfo | Noneno (None)Target zone: UTC, timezone(offset), ZoneInfo(...). None means the system local zone (a fixed-offset timezone for that moment).

Return value

datetime — An aware datetime equal to dt (same instant) whose tzinfo is tz.

Common patterns

Store UTC, display local
Convert at the edge only.
from datetime import datetime, UTC
from zoneinfo import ZoneInfo
stored = datetime.now(UTC)
shown = stored.astimezone(ZoneInfo('Europe/Paris'))
Normalize mixed offsets
Aware values from different sources, one zone for comparison and grouping.
from datetime import UTC
normalized = [dt.astimezone(UTC) for dt in parsed]
Naive value known to be UTC
Label it first — astimezone would assume local time.
from datetime import UTC
local = naive_utc.replace(tzinfo=UTC).astimezone(target_tz)

Examples

1. To UTC
from datetime import datetime, timedelta, timezone, UTC datetime(2026, 9, 29, 14, 30, tzinfo=timezone(timedelta(hours=2))).astimezone(UTC)
Returns
datetime.datetime(2026, 9, 29, 12, 30, tzinfo=datetime.timezone.utc)
2. Across midnight
from datetime import datetime, timedelta, timezone, UTC datetime(2026, 9, 29, 14, 30, tzinfo=UTC).astimezone(timezone(timedelta(hours=14)))
Returns
datetime.datetime(2026, 9, 30, 4, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=50400)))
3. Same instant, equal values
from datetime import datetime, timedelta, timezone dt = datetime(2026, 9, 29, 14, 30, tzinfo=timezone(timedelta(hours=2))) dt.astimezone(timezone(timedelta(hours=-7))) == dt
Returns
True
4. Same tzinfo: returns self
from datetime import datetime, UTC dt = datetime(2026, 9, 29, tzinfo=UTC) dt.astimezone(UTC) is dt
Returns
True
5. Overflow at the edges
from datetime import datetime, timedelta, timezone, UTC datetime.max.replace(tzinfo=UTC).astimezone(timezone(timedelta(hours=1)))
Returns
OverflowError: date value out of range
6. tz must be a tzinfo
from datetime import datetime, UTC datetime(2026, 9, 29, tzinfo=UTC).astimezone(5)
Returns
TypeError: tzinfo argument must be None or of a tzinfo subclass, not type 'int'

Pitfalls

1. astimezone to "attach" a zone
On an aware value astimezone converts; to label a naive value with the zone its fields are already in, use replace(tzinfo=...).
astimezone
from datetime import datetime, timedelta, timezone, UTC
datetime(2026, 9, 29, 12, tzinfo=UTC).astimezone(timezone(timedelta(hours=2))).hour
14
replace
from datetime import datetime, timedelta, timezone, UTC
datetime(2026, 9, 29, 12, tzinfo=UTC).replace(tzinfo=timezone(timedelta(hours=2))).hour
12
2. Converting naive UTC values
astimezone() reads a naive value as local time, so utcnow()-style values shift by the machine's offset. Mark them as UTC first — then the result no longer depends on the machine.
naive (local assumed)
from datetime import datetime
naive = datetime(2026, 9, 29, 12)
naive.tzinfo is None
True
replace(tzinfo=UTC) first
from datetime import datetime, timedelta, timezone, UTC
datetime(2026, 9, 29, 12).replace(tzinfo=UTC).astimezone(timezone(timedelta(hours=2))).isoformat()
'2026-09-29T14:00:00+02:00'

When to use

Use it
  • Showing a stored UTC value in a user's zone
  • Normalizing aware values from different offsets
Reach for something else
  • Labelling naive values → replace(tzinfo=…)
  • Server code with astimezone() and no argument — the result depends on the machine's zone setting

Notes

CPython impl
datetime_astimezone: self - utcoffset() → UTC fields, tzinfo set to tz, then tz.fromutc(); returns self when tz is self.tzinfo
No argument
astimezone() with no tz returns a fixed-offset timezone for the local zone at that moment (with the local zone name)
Naive input
Since 3.6 it can be called on a naive self, which is taken as system local time

FAQ

Make sure it is aware, then call dt.astimezone(target). For regions use zoneinfo: dt.astimezone(ZoneInfo("America/New_York")); for fixed offsets timezone(timedelta(hours=...)).