{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Type a function signature\n",
    "\n",
    "**The question.** You have a function and you want it fully typed: parameters (including defaults, `*args`, `**kwargs`), return type, and the special-case cases (generators, async, overloads). You want a single checklist so nothing gets missed.\n",
    "\n",
    "The canonical shape is: **annotate parameters after `:`, return after `->`**, use `| None` when `None` is a valid value, and prefer abstract types for parameters (`Iterable[T]`) but concrete types for returns (`list[T]`). The complete example below puts it together."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from collections.abc import Callable, Iterable\n",
    "from typing import TypeVar\n",
    "\n",
    "T = TypeVar('T')\n",
    "R = TypeVar('R')\n",
    "\n",
    "\n",
    "def batch_apply(\n",
    "    fn: Callable[[T], R],                   # function taking T, returning R\n",
    "    items: Iterable[T],                      # accepts list, tuple, generator\n",
    "    *,\n",
    "    batch_size: int = 100,                   # defaults OK; type = the non-None type\n",
    "    on_error: Callable[[T, Exception], None] | None = None,  # X | None = None\n",
    ") -> list[R]:                                # concrete return: callers can len()/index\n",
    "    \"\"\"Apply fn to each item, collecting results.\n",
    "\n",
    "    Call on_error(item, exc) on failure if provided, else re-raise.\n",
    "    \"\"\"\n",
    "    results: list[R] = []\n",
    "    for item in items:\n",
    "        try:\n",
    "            results.append(fn(item))\n",
    "        except Exception as e:\n",
    "            if on_error is None:\n",
    "                raise\n",
    "            on_error(item, e)\n",
    "    return results\n",
    "\n",
    "\n",
    "# Demo — T=int, R=str here\n",
    "def square_string(n: int) -> str:\n",
    "    return str(n * n)\n",
    "\n",
    "print(batch_apply(square_string, [1, 2, 3]))\n",
    "\n",
    "# Error path with on_error\n",
    "def failing(n: int) -> str:\n",
    "    if n == 2:\n",
    "        raise ValueError('two is forbidden')\n",
    "    return str(n)\n",
    "\n",
    "errors: list[tuple[int, str]] = []\n",
    "print(batch_apply(failing, [1, 2, 3], on_error=lambda n, e: errors.append((n, str(e)))))\n",
    "print('errors:', errors)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Variant: generators — annotate what you YIELD, not what you return\n",
    "from collections.abc import Iterator\n",
    "\n",
    "def countdown(n: int) -> Iterator[int]:\n",
    "    while n > 0:\n",
    "        yield n\n",
    "        n -= 1\n",
    "\n",
    "for i in countdown(3):\n",
    "    print(i)\n",
    "# Iterator[int] is right for the common 'yield only' case.\n",
    "# Generator[Y, S, R] matters only if .send() or a return value comes into play.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Variant: @overload — when the return type depends on the inputs\n",
    "from typing import overload\n",
    "\n",
    "@overload\n",
    "def parse(value: str) -> int: ...\n",
    "@overload\n",
    "def parse(value: str, *, as_float: bool) -> float: ...\n",
    "\n",
    "def parse(value: str, *, as_float: bool = False) -> int | float:\n",
    "    # Only the @overload stubs above are seen by callers; this is the implementation.\n",
    "    return float(value) if as_float else int(value)\n",
    "\n",
    "x = parse('42')                      # type-checker sees: int\n",
    "y = parse('3.14', as_float=True)     # type-checker sees: float\n",
    "print(x, type(x).__name__)\n",
    "print(y, type(y).__name__)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Why each piece earns its place\n",
    "\n",
    "**Abstract input, concrete output.** `Iterable[T]` for a parameter accepts any iterable — lists, tuples, sets, generators — so callers don't have to materialise their data into a list just to pass it. The return is `list[R]` because callers will want to `len(...)`, slice, or iterate twice — concrete types commit to the shape you actually deliver.\n",
    "\n",
    "**`X | None = None` has two parts.** `| None` annotates the type; `= None` is the default value. You need both. The common mistake is `timeout: float = None` — the annotation is just `float` but `None` doesn't match. `float | None = None` is the right shape.\n",
    "\n",
    "**`Callable[[T], R]`** types the function that's being passed in. The `[T]` is the parameter list, `R` is the return. `TypeVar` links the input and output types — when the caller passes a `Callable[[int], str]`, `T` becomes `int` and `R` becomes `str` throughout the signature. That lets the checker flag `batch_apply(square_string, ['a', 'b'])` as wrong: `square_string` wants `int`, not `str`.\n",
    "\n",
    "**Keyword-only is marked with `*` in the signature**, positional-only with `/`. Neither affects the annotations themselves — you annotate each parameter as normal. `batch_size` and `on_error` here are keyword-only because they sit after the `*`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Trade-offs\n",
    "\n",
    "**Generators → `Iterator[T]` for the common case.** The full type is `Generator[Yield, Send, Return]`, but you only need that when `.send()` or a meaningful return value is involved. For a plain \"yield a stream of values\" function, `Iterator[T]` is enough.\n",
    "\n",
    "**Async functions annotate the *awaited* value.** `async def fetch(url: str) -> dict` means \"awaiting this gives a dict\". The object type is technically `Coroutine[Any, Any, dict]`, but you almost never need to write that — the type-checker understands `async def`.\n",
    "\n",
    "**Don't over-specify parameters.** `list[int]` when the function only iterates is too restrictive — `Iterable[int]` works with lists, tuples, sets, generators. Broader input types are usually better for library-shaped code.\n",
    "\n",
    "**Don't under-specify returns.** A missing return annotation makes the function `Any` at every call site — the checker stops helping downstream. If the function returns nothing meaningful, annotate `-> None` explicitly.\n",
    "\n",
    "**Overloads are for cases where the return type genuinely depends on inputs.** One function, multiple signatures, one implementation at the bottom. Use them sparingly — they're a maintenance surface — but they're what lets callers see precise return types without runtime checks."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Related reading\n",
    "\n",
    "- [Type a data structure](https://agilearn.co.uk/guides/type-hints/recipes/type-a-data-structure) — how the types you pass into functions get declared.\n",
    "- [Work with optional values](https://agilearn.co.uk/guides/type-hints/recipes/work-with-optional-values) — handling `X | None` on the caller side.\n",
    "- [Avoid common typing mistakes](https://agilearn.co.uk/guides/type-hints/recipes/avoid-common-typing-mistakes) — including the `= None` vs `| None = None` trap.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}