{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "b6fae292",
   "metadata": {},
   "source": [
    "# Typing in Param\n",
    "\n",
    "Param provides a rich runtime model for validating values, and starting in version 2.4.0 it also works well with Python type checkers.\n",
    "\n",
    "This guide covers:\n",
    "\n",
    "- how types are inferred from Parameter *types* and constructor *arguments*\n",
    "- where typing support is heading next (annotation-first parameter declarations)\n",
    "\n",
    "Before we dive into the details it's important to clarify the difference between the type annotation and parameter declaration:\n",
    "\n",
    "- **type annotation** communicates intent to static tools (`Literal`, unions, etc.)\n",
    "- **Parameter declaration** controls runtime behavior and validation\n",
    "\n",
    "Throughout this guide, examples use standard `param.Parameterized` classes, and type hints are meant for tools like Pyright and mypy."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d14a8f91",
   "metadata": {},
   "outputs": [],
   "source": [
    "import param\n",
    "import typing as t\n",
    "from typing import Any, Literal\n",
    "from typing_extensions import assert_type"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "37adb4b7",
   "metadata": {},
   "source": [
    "## Type inference from Parameter types and arguments\n",
    "\n",
    "Param parameter classes encode value constraints (runtime). Thanks to Python's type system and the ability to define overloads that perform type narrowing it is often possible to infer the underlying type from the Parameter declaration.\n",
    "\n",
    "In general:\n",
    "\n",
    "- the Parameter subclass provides the base type (e.g. `Integer` -> `int`, `String` -> `str`)\n",
    "- keyword arguments can refine the type (e.g. `allow_None=True`, `item_type=int`, `class_=MyType`)\n",
    "- type checkers then infer the type of instance attributes accordingly"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c41591d4",
   "metadata": {},
   "outputs": [],
   "source": [
    "class InferredTypes(param.Parameterized):\n",
    "    title = param.String()\n",
    "    retries = param.Integer(allow_None=False)\n",
    "    timeout = param.Number(allow_None=True)\n",
    "\n",
    "    # item_type refines list element types\n",
    "    tags = param.List(item_type=str)\n",
    "\n",
    "class Model:\n",
    "    pass\n",
    "\n",
    "class MoreInferredTypes(param.Parameterized):\n",
    "    model = param.ClassSelector(class_=Model, allow_None=False, default=Model())\n",
    "    optional_model = param.ClassSelector(class_=Model, allow_None=True, default=None)\n",
    "\n",
    "\n",
    "i = InferredTypes()\n",
    "m = MoreInferredTypes()\n",
    "\n",
    "# These are checked by static type checkers (runtime no-op assertions):\n",
    "assert_type(i.title, str)\n",
    "assert_type(i.retries, int)\n",
    "assert_type(i.timeout, int | float | None)\n",
    "assert_type(i.tags, list[str])\n",
    "\n",
    "assert_type(m.model, Model)\n",
    "assert_type(m.optional_model, Model | None)\n",
    "\n",
    "\"Type assertions executed (static checking is done by your type checker).\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "97d25e7e",
   "metadata": {},
   "source": [
    "In the example above:\n",
    "\n",
    "- `param.String()` gives `str`\n",
    "- `param.Integer()` gives `int`\n",
    "- `param.Number(allow_None=True)` gives `int | float | None`\n",
    "- `param.List(item_type=str)` gives `list[str]`\n",
    "- `param.ClassSelector(class_=Model, ...)` gives `Model` (or `Model | None` when `allow_None=True`)\n",
    "\n",
    "This means Param metadata and constructor arguments can directly improve the quality of type inference."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "56c5e6bc-0d3e-4a43-834a-feecb0060ecd",
   "metadata": {},
   "source": [
    "## Choice of Type Checker\n",
    "\n",
    "The Python ecosystem now has a number of different type checking tools. Param itself is type checked against four of the most common popular type checkers, including:\n",
    "\n",
    "- `mypy`: The default type checker for Python, without a language server.\n",
    "- `pyright`: The default type checker for VSCode, including a language server.\n",
    "- `pyrefly`: A type checker written in Rust by Meta (still in beta), including a language server.\n",
    "- `ty`: A type checker written in Rust by the Astral team (still in beta), including a language server.\n",
    "\n",
    "If you are developing a library built on Param, we recommend using pyright as your type checker. Param's type annotations are primarily optimized for pyright, and it is the checker most likely to benefit your users. VSCode (via Pylance) runs pyright automatically, so correct inference will surface in your users' editors without any extra setup on their part."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8cc4ed0d",
   "metadata": {},
   "source": [
    "## Limitations\n",
    "\n",
    "Unfortunately type narrowing can only get you so far in Python's type system. Specifically `Literal` values, as in when a `Selector` parameter is bound on a `Parameterized` class cannot be automatically inferred from the `objects`, e.g.:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1a959120-6461-43f9-885f-fa7412142eb3",
   "metadata": {},
   "outputs": [],
   "source": [
    "class SelectorFromLiterals(param.Parameterized):\n",
    "    mode = param.Selector(objects=[\"train\", \"eval\"])\n",
    "\n",
    "s = SelectorFromLiterals()\n",
    "\n",
    "assert_type(s.mode, Any);"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d737dd38-feff-42d4-a8e7-d89c857f0cf6",
   "metadata": {},
   "source": [
    "For the time being we need to add a redundant type annotation (and add a corresponding `type: ignore`):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "230f399f-6012-4d00-8b62-4594e4822c5b",
   "metadata": {},
   "outputs": [],
   "source": [
    "class SelectorFromLiterals(param.Parameterized):\n",
    "    mode: Literal[\"train\", \"eval\"] = param.Selector(\n",
    "        objects=[\"train\", \"eval\"]\n",
    "    )  # type: ignore[assignment]\n",
    "\n",
    "s = SelectorFromLiterals()\n",
    "\n",
    "assert_type(s.mode, Literal[\"train\", \"eval\"]);"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "297ba6fe-996a-4f76-8875-879ea6e6959c",
   "metadata": {},
   "source": [
    "This is unfortunate and one reason why this is not the end to the typing story for Param."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "987c7a95",
   "metadata": {},
   "source": [
    "## Future direction\n",
    "\n",
    "As discussed above, the current approach to typing in Param has limits. While the proposal is still being iterated on, the prototype [PR #1066](https://github.com/holoviz/param/pull/1066) introduces a new base class that will allow inferring parameter types from the type annotations.\n",
    "\n",
    "This approach will align Param more closely with modern tooling such as dataclasses and pydantic, reduce boilerplate, and finally provide a fully typed signature and will be included in an upcoming Param 3.0 releaase."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e199907a-2b9e-4a04-a012-5500400b66b1",
   "metadata": {},
   "source": [
    "## Practical recommendations\n",
    "\n",
    "If you are updating an existing Param based codebase we recommend starting to implement typing across your codebase. While the future typing story will make certain things easier, the migration path from old-style `Parameterized` classes to the newer type annotated kind will be relatively straightforward.\n",
    "\n",
    "For now these are our practical recommendations for typing in Param:\n",
    "\n",
    "- Use specific Parameter classes (`Integer`, `String`, `ClassSelector`, etc.) instead of generic `Parameter` whenever possible.\n",
    "- Use Parameter keyword arguments (`allow_None`, `item_type`, `class_`, `bounds`) to improve both runtime checks and type precision.\n",
    "- For finite choices today, prefer `Literal[...]` annotations with `Selector`.\n",
    "- Run your type checker alongside your tests to get both static and runtime safety."
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
