{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "ee10d7a8",
   "metadata": {},
   "source": "# Time zones with `zoneinfo`\n\nA datetime without a time zone is *naive*. A datetime with one is *aware*. Python treats the two as different enough that it refuses to compare or arithmetic them together.\n\nThis notebook introduces `zoneinfo` — the standard-library module (Python 3.9+) for time zone handling — and shows how to create, convert, and compare time-zone-aware datetimes correctly."
  },
  {
   "cell_type": "markdown",
   "id": "652a48df",
   "metadata": {},
   "source": "## Naive versus aware\n\nA naive datetime doesn't know what time zone it represents. `datetime(2026, 4, 21, 14, 30)` might be 14:30 in London, or in Tokyo, or anywhere — the object carries no information."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c8346788",
   "metadata": {},
   "outputs": [],
   "source": "from datetime import datetime\n\nnaive = datetime(2026, 4, 21, 14, 30)\nprint(naive)\nprint(naive.tzinfo)       # None — it's naive"
  },
  {
   "cell_type": "markdown",
   "id": "c60e68ad",
   "metadata": {},
   "source": "An aware datetime has a `tzinfo` attribute telling you which zone it's in. Use `zoneinfo.ZoneInfo` to build one from an IANA time zone name (the `Continent/City` form)."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "26f03201",
   "metadata": {},
   "outputs": [],
   "source": "from zoneinfo import ZoneInfo\n\nlondon = ZoneInfo(\"Europe/London\")\naware = datetime(2026, 4, 21, 14, 30, tzinfo=london)\nprint(aware)\nprint(aware.tzinfo)"
  },
  {
   "cell_type": "markdown",
   "id": "d2a70070",
   "metadata": {},
   "source": "The IANA database covers every inhabited region on Earth. A few names worth knowing: `UTC`, `Europe/London`, `America/New_York`, `Asia/Tokyo`, `Australia/Sydney`. Never use abbreviations like `\"BST\"` or `\"EST\"` — they're ambiguous (`EST` means something different in the US and Australia). `ZoneInfo(\"UTC\")` is correct; `ZoneInfo(\"GMT\")` is also valid but you'll see UTC far more often."
  },
  {
   "cell_type": "markdown",
   "id": "9bd26639",
   "metadata": {},
   "source": "## Converting between time zones\n\n`.astimezone(target)` converts an aware datetime to another time zone. The absolute moment doesn't change — only the presentation does."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0b5b6c80",
   "metadata": {},
   "outputs": [],
   "source": "utc = ZoneInfo(\"UTC\")\ntokyo = ZoneInfo(\"Asia/Tokyo\")\nnew_york = ZoneInfo(\"America/New_York\")\n\nmoment = datetime(2026, 4, 21, 14, 30, tzinfo=utc)\n\nprint(moment.astimezone(london))\nprint(moment.astimezone(tokyo))\nprint(moment.astimezone(new_york))"
  },
  {
   "cell_type": "markdown",
   "id": "81078217",
   "metadata": {},
   "source": "All three printouts represent the same instant in time — just expressed in different local clocks. Subtracting any of them from each other gives `timedelta(0)`."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c778b68d",
   "metadata": {},
   "outputs": [],
   "source": "a = moment.astimezone(london)\nb = moment.astimezone(tokyo)\nprint(a - b)"
  },
  {
   "cell_type": "markdown",
   "id": "cfb48ebb",
   "metadata": {},
   "source": "## The naive/aware error\n\nYou can't compare, subtract, or order a naive datetime against an aware one. Python refuses outright, because the answer depends on a time zone it wasn't told."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2dc02e58",
   "metadata": {},
   "outputs": [],
   "source": "try:\n    aware - naive\nexcept TypeError as e:\n    print(f\"{type(e).__name__}: {e}\")"
  },
  {
   "cell_type": "markdown",
   "id": "000d7477",
   "metadata": {},
   "source": "This is a frequent source of bugs in codebases that mix the two. The usual fix is to make *everything* aware — see the [UTC everywhere concept essay](https://agilearn.co.uk/guides/dates-and-times/concepts/utc-everywhere). Attach a time zone on the way in and you never have to worry about it again."
  },
  {
   "cell_type": "markdown",
   "id": "320755f9",
   "metadata": {},
   "source": "## Getting the current aware datetime\n\n`datetime.now()` with no argument is naive — avoid it. `datetime.now(tz=...)` is aware. `datetime.utcnow()` exists too, but returns a naive datetime set to UTC — arguably the worst of both worlds. Don't use it."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5d8cd07f",
   "metadata": {},
   "outputs": [],
   "source": "# Good: aware datetime in UTC\nnow_utc = datetime.now(tz=utc)\nprint(now_utc)\nprint(now_utc.tzinfo)\n\n# Also fine: aware datetime in a specific zone\nnow_london = datetime.now(tz=london)\nprint(now_london)"
  },
  {
   "cell_type": "markdown",
   "id": "c8fddb9f",
   "metadata": {},
   "source": "## Daylight saving transitions\n\n`zoneinfo` handles DST automatically, using the IANA database. Two moments to watch for:\n\n- The **spring forward** gap — an hour that doesn't exist. In London, 01:30 on the day the clocks go forward isn't a real moment.\n- The **autumn fall-back** overlap — an hour that happens twice. 01:30 on the day the clocks go back happens once before and once after the transition.\n\n`zoneinfo` picks a reasonable default for each (the interpretation before the transition), but if you care, you can be explicit via the `fold` attribute. Most application code doesn't need to:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b50104d9",
   "metadata": {},
   "outputs": [],
   "source": "# Spring forward in London: 2026-03-29 01:00 skips to 02:00.\n# Just before: 00:30 BST→UTC... actually, just before is GMT. Let's\n# just check what zoneinfo says about these two moments around the transition.\nbefore = datetime(2026, 3, 29, 0, 30, tzinfo=london)\nafter = datetime(2026, 3, 29, 2, 30, tzinfo=london)\nprint(\"UTC offsets:\", before.utcoffset(), after.utcoffset())\nprint(\"Actual gap:\", after - before)          # 2 hours clock, but only 1 real"
  },
  {
   "cell_type": "markdown",
   "id": "1d06a75c",
   "metadata": {},
   "source": "The UTC offset flips from `0:00:00` to `1:00:00` across the transition — that's the DST change. The \"2 hours\" reported is the elapsed clock time, which correctly represents 1 hour of real elapsed time because we skipped an hour on the clock.\n\nIn the autumn, the inverse happens — same logic, other direction. See the [avoid common date/time mistakes recipe](https://agilearn.co.uk/guides/dates-and-times/recipes/avoid-common-datetime-mistakes) for the specific traps."
  },
  {
   "cell_type": "markdown",
   "id": "2b8555c5",
   "metadata": {},
   "source": "## Serialising aware datetimes\n\nISO 8601 with a UTC offset is the sane choice:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9608c485",
   "metadata": {},
   "outputs": [],
   "source": "moment = datetime(2026, 4, 21, 14, 30, tzinfo=london)\nprint(moment.isoformat())        # includes the offset\n\n# Round-trip\nparsed = datetime.fromisoformat(moment.isoformat())\nprint(parsed)\nprint(parsed.tzinfo)"
  },
  {
   "cell_type": "markdown",
   "id": "099bdd39",
   "metadata": {},
   "source": "The round-tripped value has a `timezone` fixed-offset, not a `ZoneInfo` — enough to represent the same instant, but stripped of the zone's name and DST rules. If you need the zone name, store it separately or re-attach `ZoneInfo` after parsing."
  },
  {
   "cell_type": "markdown",
   "id": "030afead",
   "metadata": {},
   "source": "## Exercise\n\nA meeting is scheduled for `2026-09-15 15:00` London time. Write code that:\n\n1. Constructs the aware datetime for the meeting.\n2. Prints the equivalent local time for New York and Tokyo.\n3. Calculates how many hours until the meeting, given it's currently `2026-09-14 22:00` UTC.\n\nUse `ZoneInfo` for the zones."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "083a186b",
   "metadata": {},
   "outputs": [],
   "source": "# Your code here\n"
  },
  {
   "cell_type": "markdown",
   "id": "6161c261",
   "metadata": {},
   "source": "<details>\n<summary>Solution</summary>\n\n```python\nfrom datetime import datetime, timezone\nfrom zoneinfo import ZoneInfo\n\nlondon = ZoneInfo(\"Europe/London\")\nnew_york = ZoneInfo(\"America/New_York\")\ntokyo = ZoneInfo(\"Asia/Tokyo\")\n\nmeeting = datetime(2026, 9, 15, 15, 0, tzinfo=london)\nprint(f\"New York: {meeting.astimezone(new_york)}\")\nprint(f\"Tokyo:    {meeting.astimezone(tokyo)}\")\n\nnow = datetime(2026, 9, 14, 22, 0, tzinfo=timezone.utc)\ngap = meeting - now\nprint(f\"Hours until meeting: {gap.total_seconds() / 3600}\")\n```\n</details>"
  },
  {
   "cell_type": "markdown",
   "id": "ddc78d57",
   "metadata": {},
   "source": "## Recap\n\n- Naive datetimes carry no time zone; aware ones do via `tzinfo`.\n- Use `zoneinfo.ZoneInfo(\"Region/City\")` to build `tzinfo` objects from the IANA database.\n- `.astimezone(target)` converts between zones. The instant doesn't change, the presentation does.\n- Avoid `datetime.now()` (naive) and `datetime.utcnow()` (naive but UTC). Use `datetime.now(tz=...)`.\n- Python refuses to compare or subtract naive against aware — a feature, not a bug.\n- DST is handled by `zoneinfo` via the IANA database; the edge cases are in the [avoid common mistakes recipe](https://agilearn.co.uk/guides/dates-and-times/recipes/avoid-common-datetime-mistakes).\n\nThat's the core of `datetime`. The [recipes](https://agilearn.co.uk/guides/dates-and-times/recipes) and [reference](https://agilearn.co.uk/guides/dates-and-times/reference) pages go deeper into specific tasks."
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}