{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Convert between time zones\n",
    "\n",
    "**The question.** You have a datetime in one zone — UTC for storage, or local for a user — and you need it in another. Maybe to display a stored UTC timestamp in London time, or to store a user's entered local time in UTC.\n",
    "\n",
    "The two-line pattern: **attach a zone to make the datetime aware, then call `.astimezone()` to convert**. Every cross-zone conversion in modern Python (3.9+) uses this shape."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from datetime import datetime\n",
    "from zoneinfo import ZoneInfo\n",
    "\n",
    "utc      = ZoneInfo('UTC')\n",
    "london   = ZoneInfo('Europe/London')\n",
    "new_york = ZoneInfo('America/New_York')\n",
    "\n",
    "\n",
    "# UTC -> local for display\n",
    "stored  = datetime(2026, 4, 21, 14, 30, tzinfo=utc)\n",
    "display = stored.astimezone(london)\n",
    "print('UTC stored:', stored)\n",
    "print('London:    ', display)\n",
    "print('formatted: ', display.strftime('%d %B %Y, %H:%M %Z'))  # %Z prints the zone\n",
    "\n",
    "\n",
    "# Local -> UTC for storage (attach the user's zone, then convert)\n",
    "entered     = datetime(2026, 9, 15, 15, 0, tzinfo=london)\n",
    "for_storage = entered.astimezone(utc)\n",
    "print('\\nuser entered (London):', entered)\n",
    "print('for storage (UTC):    ', for_storage)\n",
    "\n",
    "\n",
    "# Comparing aware datetimes across zones — Python normalises to UTC\n",
    "london_mtg = datetime(2026, 9, 15, 15, 0, tzinfo=london)\n",
    "ny_mtg     = datetime(2026, 9, 15,  9, 0, tzinfo=new_york)\n",
    "print('\\nlondon > ny?', london_mtg > ny_mtg)       # True: 14:00 UTC > 13:00 UTC\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Variant: 'the same wall-clock time in every zone' — per-user 09:00 local\n",
    "users = {\n",
    "    'alice':  london,\n",
    "    'bob':    new_york,\n",
    "    'carmen': ZoneInfo('Asia/Tokyo'),\n",
    "}\n",
    "\n",
    "for name, zone in users.items():\n",
    "    local_9am = datetime(2026, 9, 15, 9, 0, tzinfo=zone)\n",
    "    print(f'{name:8} local 09:00 = {local_9am.astimezone(utc)} UTC')\n",
    "\n",
    "# Three different UTC instants — schedule each user's reminder separately.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# DST watch-out: attach the zone BEFORE arithmetic, not after\n",
    "from datetime import timedelta\n",
    "\n",
    "mtg = datetime(2026, 3, 29, 9, 0, tzinfo=london)   # day BST starts\n",
    "print('Meeting:       ', mtg.astimezone(utc))                     # 08:00 UTC (BST)\n",
    "print('Same meeting -1d:', (mtg - timedelta(days=1)).astimezone(utc))  # 09:00 UTC (GMT)\n",
    "# The UTC offset shifts because we crossed the spring-forward boundary.\n",
    "# Elapsed real time is still 24 hours; the clock representation changes.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Why it works\n",
    "\n",
    "A datetime is either **naive** (no zone attached — just a string of digits) or **aware** (knows which zone it's in). Arithmetic and comparison on aware datetimes are correct across zones; the same operations on naive ones are silently wrong as soon as a DST transition shows up. `.astimezone(zone)` is defined only on aware datetimes — it computes *the same real-world moment, rendered in the target zone*.\n",
    "\n",
    "`zoneinfo.ZoneInfo` (3.9+) reads the IANA timezone database that your operating system already has. IANA names like `Europe/London` know about DST boundaries, historical offset changes, and leap seconds. Fixed offsets (`+01:00`) don't — they're frozen in time and will silently mis-handle the next spring-forward. Always name the zone; never hard-code the offset.\n",
    "\n",
    "`%Z` in a `strftime` format string emits the zone name (`BST`, `GMT`, `EDT`), which matters in UIs so users know which zone they're looking at."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Trade-offs\n",
    "\n",
    "**Store in UTC, convert on display.** UTC doesn't have DST, doesn't depend on a user's machine settings, and compares trivially. Any other storage convention is a footgun waiting to go off. For the full argument, see the [UTC everywhere essay](https://agilearn.co.uk/guides/dates-and-times/concepts/utc-everywhere).\n",
    "\n",
    "**Attach the zone *before* arithmetic.** `datetime(2026, 3, 29, 9, 0) - timedelta(days=1)` then `.replace(tzinfo=london)` gives wrong results around DST boundaries. `datetime(2026, 3, 29, 9, 0, tzinfo=london) - timedelta(days=1)` gives correct ones. The rule: zone first, then arithmetic.\n",
    "\n",
    "**`.replace(tzinfo=zone)` is almost always wrong.** It *claims* the datetime is in that zone without checking. Use it to attach a zone to a naive datetime that you know is in that zone; never use it to convert. For actual conversion, always use `.astimezone(zone)`.\n",
    "\n",
    "**`datetime.utcnow()` is deprecated in 3.12 and returns a *naive* datetime** anyway. Use `datetime.now(tz=ZoneInfo('UTC'))` for the current UTC instant — aware, correct, obvious."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Related reading\n",
    "\n",
    "- [UTC everywhere](https://agilearn.co.uk/guides/dates-and-times/concepts/utc-everywhere) — the design essay for the *\"why store UTC?\"* question.\n",
    "- [Avoid common datetime mistakes](https://agilearn.co.uk/guides/dates-and-times/recipes/avoid-common-datetime-mistakes) — including naive/aware mixing and `utcnow()`.\n",
    "- [Time-zone aware formatting](https://agilearn.co.uk/guides/dates-and-times/reference/time-zone-aware-formatting) — every `%Z`/`%z` subtlety.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}