{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "bbaa64d8",
   "metadata": {},
   "source": "# Basic annotations\n\nThe syntax for annotating variables, function parameters, and return types. This notebook covers the shapes you'll use on 90% of everyday code."
  },
  {
   "cell_type": "markdown",
   "id": "221d990e",
   "metadata": {},
   "source": "## Function parameters and return types\n\nPut the type after a colon for parameters, and after a `->` for the return:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b60c4c34",
   "metadata": {},
   "outputs": [],
   "source": "def area(width: float, height: float) -> float:\n    return width * height\n\nprint(area(3.0, 4.5))"
  },
  {
   "cell_type": "markdown",
   "id": "2f3d2f97",
   "metadata": {},
   "source": "A function that returns nothing meaningful should be annotated `-> None`:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f766a88f",
   "metadata": {},
   "outputs": [],
   "source": "def log(message: str) -> None:\n    print(f\"[log] {message}\")\n\nlog(\"saved\")"
  },
  {
   "cell_type": "markdown",
   "id": "33d0c086",
   "metadata": {},
   "source": "`None` as a return annotation means \"this function is called for its side effect and returns nothing\". A function with no `-> ...` at all is technically valid Python, but the type-checker will treat its return as `Any` — better to be explicit."
  },
  {
   "cell_type": "markdown",
   "id": "e91f0881",
   "metadata": {},
   "source": "## The built-in types\n\nThe usual suspects, and the names you use to annotate them:\n\n| Type | Annotation | Example |\n| --- | --- | --- |\n| Integer | `int` | `42` |\n| Float | `float` | `3.14` |\n| String | `str` | `\"hello\"` |\n| Boolean | `bool` | `True`, `False` |\n| Bytes | `bytes` | `b\"\\x00\\x01\"` |\n| None | `None` | `None` |\n\nNote that `bool` is technically a subclass of `int` in Python — type-checkers let you pass a `bool` where an `int` is expected, but not the other way around. `1 + True == 2` works for this reason (though most of the time you shouldn't rely on it)."
  },
  {
   "cell_type": "markdown",
   "id": "21b4f326",
   "metadata": {},
   "source": "## Variable annotations\n\nYou can annotate variables too, though they're less often necessary because type-checkers infer the type from the assignment. The syntax:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8c504771",
   "metadata": {},
   "outputs": [],
   "source": "count: int = 0\nname: str = \"Matthew\"\npi: float = 3.14159\n\nprint(count, name, pi)"
  },
  {
   "cell_type": "markdown",
   "id": "82a9807f",
   "metadata": {},
   "source": "When is an annotation useful even though it's inferrable?\n\n- **The initial value is ambiguous.** `results: list[int] = []` tells the type-checker that the list will hold `int`s — otherwise the empty list has inferred type `list[Any]`.\n- **You want to document an invariant.** `timeout: float = 30` is more readable than `timeout = 30.0`, and protects against someone later writing `timeout = \"30\"` thinking strings are fine.\n- **Declaring without initialising.** `processed: int` (no value) introduces a name and its type without actually binding anything to it yet. This is occasionally useful in classes."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8793ac83",
   "metadata": {},
   "outputs": [],
   "source": "# Ambiguous without annotation\nresults: list[int] = []\n\n# The type-checker will now flag:\n# results.append(\"string\")   # error: expected int, got str\n\nresults.append(1)\nresults.append(2)\nprint(results)"
  },
  {
   "cell_type": "markdown",
   "id": "51860bbf",
   "metadata": {},
   "source": "## Default arguments\n\nType comes before the default:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6d2534dd",
   "metadata": {},
   "outputs": [],
   "source": "def greet(name: str, greeting: str = \"Hello\") -> str:\n    return f\"{greeting}, {name}\"\n\nprint(greet(\"Alice\"))\nprint(greet(\"Alice\", \"Hi\"))"
  },
  {
   "cell_type": "markdown",
   "id": "1fa523a1",
   "metadata": {},
   "source": "The default doesn't have to be the same type as the annotation — the `str = \"Hello\"` says \"this parameter is of type `str`, and its default is the string `\"Hello\"`\". The annotation and the default are separate statements."
  },
  {
   "cell_type": "markdown",
   "id": "82de045a",
   "metadata": {},
   "source": "## Keyword-only and positional-only\n\n`*args` and `**kwargs` follow the same pattern:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d4f2c1bc",
   "metadata": {},
   "outputs": [],
   "source": "def build_url(base: str, *parts: str, **params: str) -> str:\n    path = \"/\".join([base.rstrip(\"/\")] + list(parts))\n    if params:\n        query = \"&\".join(f\"{k}={v}\" for k, v in params.items())\n        path = f\"{path}?{query}\"\n    return path\n\nprint(build_url(\"https://example.com\", \"users\", \"42\", sort=\"name\", limit=\"10\"))"
  },
  {
   "cell_type": "markdown",
   "id": "edc29d06",
   "metadata": {},
   "source": "The annotation on `*args` or `**kwargs` describes the type of **each individual element**, not the tuple/dict itself. `*parts: str` means \"each arg in `parts` is a `str`\" — inside the function, `parts` has type `tuple[str, ...]`."
  },
  {
   "cell_type": "markdown",
   "id": "18a66277",
   "metadata": {},
   "source": "## Multiple types via union (`|`)\n\nIf a parameter can be one of several types, use `X | Y`. A common case is \"this thing or `None`\":"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c499c260",
   "metadata": {},
   "outputs": [],
   "source": "def format_price(pence: int, currency: str | None = None) -> str:\n    amount = f\"£{pence / 100:.2f}\"\n    if currency is None:\n        return amount\n    return f\"{amount} ({currency})\"\n\nprint(format_price(1250))\nprint(format_price(1250, \"GBP\"))"
  },
  {
   "cell_type": "markdown",
   "id": "54dda6fb",
   "metadata": {},
   "source": "`str | None` means \"either a string or `None`\". Using `None` as a marker for \"no value provided\" is idiomatic in Python; `X | None` is how you type it.\n\nOther unions come up too — `int | float`, `str | bytes`, `dict | list` — all work the same way. On Python 3.10+ the `|` syntax is preferred. On older versions, use `Union[X, Y]` and `Optional[X]` from the `typing` module."
  },
  {
   "cell_type": "markdown",
   "id": "424e1421",
   "metadata": {},
   "source": "## Custom types\n\nYour own classes work exactly like the built-ins — use the class name as the annotation:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6261f025",
   "metadata": {},
   "outputs": [],
   "source": "from dataclasses import dataclass\n\n@dataclass\nclass Point:\n    x: float\n    y: float\n\ndef distance_from_origin(p: Point) -> float:\n    return (p.x ** 2 + p.y ** 2) ** 0.5\n\nprint(distance_from_origin(Point(3.0, 4.0)))"
  },
  {
   "cell_type": "markdown",
   "id": "0e6402e6",
   "metadata": {},
   "source": "The `Point` in `p: Point` is the literal class. If you move the class to a different module, your annotations follow the usual import rules."
  },
  {
   "cell_type": "markdown",
   "id": "c2f3ff48",
   "metadata": {},
   "source": "## Using `Any` as an escape hatch\n\nWhen you can't (or don't want to) give something a precise type, `Any` from the `typing` module means \"anything is permitted here; don't check\":"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d6f4586f",
   "metadata": {},
   "outputs": [],
   "source": "from typing import Any\n\ndef store(key: str, value: Any) -> None:\n    # Can't predict what callers will store\n    pass\n\nstore(\"x\", 42)\nstore(\"y\", \"hello\")\nstore(\"z\", [1, 2, 3])"
  },
  {
   "cell_type": "markdown",
   "id": "8c4225aa",
   "metadata": {},
   "source": "`Any` is the type-checker's off-switch. It's fine to use — that's what gradual typing is for — but every `Any` is a place where type errors can slip past. Reach for it when a narrower type genuinely doesn't exist, not as a way to avoid thinking about the type."
  },
  {
   "cell_type": "markdown",
   "id": "b53f5efd",
   "metadata": {},
   "source": "## Exercise\n\nAnnotate the following functions with appropriate type hints. Each has a comment describing what it does. Aim for the most specific type that's accurate."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c9856cd0",
   "metadata": {},
   "outputs": [],
   "source": "# Fill in the annotations\n\ndef to_upper(text):\n    \"\"\"Return the text uppercased.\"\"\"\n    return text.upper()\n\ndef is_even(n):\n    \"\"\"True if n is an even integer.\"\"\"\n    return n % 2 == 0\n\ndef average(numbers, default=None):\n    \"\"\"Mean of a list of numbers, or `default` if the list is empty.\"\"\"\n    if not numbers:\n        return default\n    return sum(numbers) / len(numbers)\n\ndef load_config(path, strict=False):\n    \"\"\"Load a config from a file path; raise on missing keys if strict is True.\"\"\"\n    # Pretend-implementation\n    return {\"host\": \"localhost\", \"port\": 8080}"
  },
  {
   "cell_type": "markdown",
   "id": "6ae9078e",
   "metadata": {},
   "source": "<details>\n<summary>Solution</summary>\n\n```python\ndef to_upper(text: str) -> str:\n    return text.upper()\n\ndef is_even(n: int) -> bool:\n    return n % 2 == 0\n\ndef average(numbers: list[float], default: float | None = None) -> float | None:\n    if not numbers:\n        return default\n    return sum(numbers) / len(numbers)\n\ndef load_config(path: str, strict: bool = False) -> dict[str, str | int]:\n    return {\"host\": \"localhost\", \"port\": 8080}\n```\n\nA few things to note:\n\n- `average` returns `float | None` — if the list is empty, the return is the default (which might be `None`). Reflecting that in the return type is honest.\n- `load_config`'s return is `dict[str, str | int]` — the values are mixed types. We'll see cleaner ways to type heterogeneous dicts (`TypedDict`) in a later notebook.\n- `list[float]` accepts `list[int]` too, because `int` is considered a subtype of `float` by most type-checkers.\n</details>"
  },
  {
   "cell_type": "markdown",
   "id": "8c815f69",
   "metadata": {},
   "source": "## Recap\n\n- Parameter annotations go after `:`, return annotation goes after `->`.\n- Annotate variables with `name: type = value` — useful when the inferred type would be ambiguous.\n- `None` is the return annotation for functions that return nothing.\n- `X | Y` is a union of types; `X | None` is the standard \"optional\" shape.\n- `Any` is an escape hatch — use when a narrower type doesn't exist.\n\nNext: [Generics and collections](https://agilearn.co.uk/guides/type-hints/learn/03-generics-and-collections) — typing lists, dicts, tuples, and the elements they contain."
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}