{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "bc7d0243",
   "metadata": {},
   "source": "# Why type hints?\n\nPython is dynamically typed: variables don't have declared types, and the interpreter figures out what operations are valid at runtime. This is flexible, but it means the program can get some way through an execution before something explodes.\n\nType *hints* — the annotations you'll see in modern Python code — describe the types you intend things to have. Python itself doesn't use them for anything at runtime. A separate tool (`mypy`, `pyright`, or similar) reads them and flags mismatches *before* you run the code. Your editor uses them too, for autocomplete and inline warnings.\n\nThis notebook covers what type hints are, what they aren't, and the problem they solve."
  },
  {
   "cell_type": "markdown",
   "id": "cd58427b",
   "metadata": {},
   "source": "## What a type hint looks like\n\nA function with no hints:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dfc99d51",
   "metadata": {},
   "outputs": [],
   "source": "def greet(name):\n    return f\"Hello, {name}\"\n\nprint(greet(\"Matthew\"))"
  },
  {
   "cell_type": "markdown",
   "id": "211e520f",
   "metadata": {},
   "source": "The same function with type hints — `name: str` means \"`name` should be a string\" and `-> str` means \"the return value is a string\":"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0c3b2f1e",
   "metadata": {},
   "outputs": [],
   "source": "def greet(name: str) -> str:\n    return f\"Hello, {name}\"\n\nprint(greet(\"Matthew\"))"
  },
  {
   "cell_type": "markdown",
   "id": "87d699e2",
   "metadata": {},
   "source": "The behaviour at runtime is *identical* — Python runs both functions exactly the same way. The only difference is the annotations, which are there for humans and tooling to read."
  },
  {
   "cell_type": "markdown",
   "id": "b98a0728",
   "metadata": {},
   "source": "## Python doesn't enforce type hints\n\nThis is worth being clear about. Annotations are just metadata — Python stores them on the function object but doesn't check anything. You can pass the \"wrong\" type and Python will run the function anyway, raising an error (or not) only if the function body actually tries something the type doesn't support:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "46c09536",
   "metadata": {},
   "outputs": [],
   "source": "def greet(name: str) -> str:\n    return f\"Hello, {name}\"\n\n# Passing an int — Python doesn't care\nprint(greet(42))\n\n# The annotations are still stored, though\nprint(greet.__annotations__)"
  },
  {
   "cell_type": "markdown",
   "id": "4d96a0bb",
   "metadata": {},
   "source": "That `42` ran just fine because f-strings happily accept any type. The annotation said \"this should be a string\"; Python didn't check; the code still worked. This is central to how Python type hints are designed — they're *declarative*, not *enforcing*."
  },
  {
   "cell_type": "markdown",
   "id": "9b50669f",
   "metadata": {},
   "source": "## Where the value comes from\n\nTwo audiences make annotations useful:\n\n**A type-checker** (mypy, pyright) reads annotations across your whole codebase and reports mismatches — \"you called `greet(42)` but `greet` expects a `str`\". Run this in CI and bugs get caught before the code ever executes.\n\n**Your editor** uses them for autocomplete (\"what methods can I call on this?\") and for inline warnings (\"this call won't type-check\"). VS Code's Pylance, PyCharm's inspector, Zed's LSP — they all do this. You see the benefit without running anything.\n\nA function with good type hints is also self-documenting — the reader doesn't have to trace through the body to learn what shape the inputs and outputs are."
  },
  {
   "cell_type": "markdown",
   "id": "34617aca",
   "metadata": {},
   "source": "## Running `mypy`\n\nYou can't run mypy in a Jupyter cell easily, but it's worth seeing what it catches. Given this function in a file `greet.py`:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5bcae581",
   "metadata": {},
   "outputs": [],
   "source": "# Contents of greet.py:\nsource = '''\ndef greet(name: str) -> str:\n    return f\"Hello, {name}\"\n\ngreet(42)          # should be str, not int\n'''\n\n# Running `mypy greet.py` would produce:\nprint('greet.py:5: error: Argument 1 to \"greet\" has incompatible type \"int\"; expected \"str\"')\nprint('Found 1 error in 1 file (checked 1 source file)')"
  },
  {
   "cell_type": "markdown",
   "id": "ced603d5",
   "metadata": {},
   "source": "No code has been executed. `mypy` analyses the source, sees the mismatch, and reports it. That's the whole loop: write code with annotations, run a checker, fix what it flags.\n\nMost teams wire this into pre-commit or CI so the checks happen automatically."
  },
  {
   "cell_type": "markdown",
   "id": "4eed2665",
   "metadata": {},
   "source": "## What problems this actually solves\n\nType hints are most useful when they catch bugs that would otherwise surface as confusing runtime errors far from the original mistake. Canonical examples:"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6a1f7266",
   "metadata": {},
   "outputs": [],
   "source": "# Imagine a real codebase — functions call each other, data flows around.\n# A function signature change in one place can break callers in many places.\n\ndef parse_user_id(raw: str) -> int:\n    return int(raw.strip())\n\ndef fetch_user(user_id: int) -> dict:\n    # ... imagine a database lookup ...\n    return {\"id\": user_id, \"name\": \"Alice\"}\n\n# If someone refactors parse_user_id to return a str, every caller of fetch_user\n# that uses its output is now broken. A type-checker flags this immediately.\n# Without type-checking, you'd find out when a test (or worse, production) failed.\n\nraw = \"42\"\nuid = parse_user_id(raw)\nuser = fetch_user(uid)\nprint(user)"
  },
  {
   "cell_type": "markdown",
   "id": "b1a41cf3",
   "metadata": {},
   "source": "The bugs that get caught tend to be:\n\n- Passing the wrong type to a function (often the most useful category).\n- Forgetting to handle `None` returns.\n- Misremembering the shape of a data structure (\"is this a `list[dict]` or a `dict[str, list]`?\").\n- Refactoring broken contracts — changing one function's signature and missing the callers that depended on it.\n\nThings type hints *don't* catch: logic errors, off-by-ones, using the wrong algorithm. A type-checker verifies the shapes fit, not that the code does what it should."
  },
  {
   "cell_type": "markdown",
   "id": "fc86274b",
   "metadata": {},
   "source": "## The cost\n\nAnnotations take time to write. They can clutter simple code (`def add(a: int, b: int) -> int: return a + b`). They sometimes get awkward — typing a function that accepts \"anything with a `.read()` method\" needs a `Protocol`, which is more typing than the function itself.\n\nFor short scripts or one-off notebooks, the cost-benefit is often not there. For libraries, long-lived applications, and anything that multiple people will touch, type hints pay for themselves quickly — the [when type hints help essay](https://agilearn.co.uk/guides/type-hints/concepts/when-type-hints-help) has the full argument."
  },
  {
   "cell_type": "markdown",
   "id": "ceb80b12",
   "metadata": {},
   "source": "## Gradual typing\n\nPython's type system is *gradual*: you don't have to type everything. Annotate the parts you care about, leave the rest alone, and the type-checker silently treats untyped code as `Any` (everything is permitted, nothing is checked).\n\nThis is a real feature. It means you can add types to a legacy codebase one module at a time, or leave your test files unannotated while still typing the production code carefully. See the [gradual typing essay](https://agilearn.co.uk/guides/type-hints/concepts/gradual-typing) for what this buys you."
  },
  {
   "cell_type": "markdown",
   "id": "745f842c",
   "metadata": {},
   "source": "## Recap\n\n- Type hints are annotations — they describe intended types but don't affect runtime behaviour.\n- A *type-checker* (mypy, pyright) reads them and flags mismatches before execution.\n- Editors use them for autocomplete and inline warnings.\n- They catch type-shape bugs, not logic bugs. Worth the cost on long-lived code; skip for one-off scripts.\n- Gradual: annotate some things, not others — a legit workflow.\n\nNext: [Basic annotations](https://agilearn.co.uk/guides/type-hints/learn/02-basic-annotations), the syntax for typing variables, parameters, and return values."
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}