Typing in Param#

Param provides a rich runtime model for validating values, and starting in version 2.4.0 it also works well with Python type checkers.

This guide covers:

  • how types are inferred from Parameter types and constructor arguments

  • where typing support is heading next (annotation-first parameter declarations)

Before we dive into the details it’s important to clarify the difference between the type annotation and parameter declaration:

  • type annotation communicates intent to static tools (Literal, unions, etc.)

  • Parameter declaration controls runtime behavior and validation

Throughout this guide, examples use standard param.Parameterized classes, and type hints are meant for tools like Pyright and mypy.

import param
import typing as t
from typing import Any, Literal
from typing_extensions import assert_type

Type inference from Parameter types and arguments#

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.

In general:

  • the Parameter subclass provides the base type (e.g. Integer -> int, String -> str)

  • keyword arguments can refine the type (e.g. allow_None=True, item_type=int, class_=MyType)

  • type checkers then infer the type of instance attributes accordingly

class InferredTypes(param.Parameterized):
    title = param.String()
    retries = param.Integer(allow_None=False)
    timeout = param.Number(allow_None=True)

    # item_type refines list element types
    tags = param.List(item_type=str)

class Model:
    pass

class MoreInferredTypes(param.Parameterized):
    model = param.ClassSelector(class_=Model, allow_None=False, default=Model())
    optional_model = param.ClassSelector(class_=Model, allow_None=True, default=None)


i = InferredTypes()
m = MoreInferredTypes()

# These are checked by static type checkers (runtime no-op assertions):
assert_type(i.title, str)
assert_type(i.retries, int)
assert_type(i.timeout, int | float | None)
assert_type(i.tags, list[str])

assert_type(m.model, Model)
assert_type(m.optional_model, Model | None)

"Type assertions executed (static checking is done by your type checker)."
'Type assertions executed (static checking is done by your type checker).'

In the example above:

  • param.String() gives str

  • param.Integer() gives int

  • param.Number(allow_None=True) gives int | float | None

  • param.List(item_type=str) gives list[str]

  • param.ClassSelector(class_=Model, ...) gives Model (or Model | None when allow_None=True)

This means Param metadata and constructor arguments can directly improve the quality of type inference.

Choice of Type Checker#

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:

  • mypy: The default type checker for Python, without a language server.

  • pyright: The default type checker for VSCode, including a language server.

  • pyrefly: A type checker written in Rust by Meta, including a language server.

  • ty: A type checker written in Rust by the Astral team (still in beta), including a language server.

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.

Limitations#

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.:

class SelectorFromLiterals(param.Parameterized):
    mode = param.Selector(objects=["train", "eval"])

s = SelectorFromLiterals()

assert_type(s.mode, Any);

For the time being we need to add a redundant type annotation (and add a corresponding type: ignore):

class SelectorFromLiterals(param.Parameterized):
    mode: Literal["train", "eval"] = param.Selector(
        objects=["train", "eval"]
    )  # type: ignore[assignment]

s = SelectorFromLiterals()

assert_type(s.mode, Literal["train", "eval"]);

This is unfortunate and one reason why this is not the end to the typing story for Param.

Future direction#

As discussed above, the current approach to typing in Param has limits. While the proposal is still being iterated on, the prototype PR #1066 introduces a new base class that will allow inferring parameter types from the type annotations.

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 release.

Practical recommendations#

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.

For now these are our practical recommendations for typing in Param:

  • Use specific Parameter classes (Integer, String, ClassSelector, etc.) instead of generic Parameter whenever possible.

  • Use Parameter keyword arguments (allow_None, item_type, class_, bounds) to improve both runtime checks and type precision.

  • For finite choices today, prefer Literal[...] annotations with Selector.

  • Run your type checker alongside your tests to get both static and runtime safety.

Tool specific information#

Below is giving tool specific information.

Mypy#

If you use mypy, we recommend enabling the param mypy plugin. Add the following to your pyproject.toml:

[tool.mypy]
plugins = ["param.mypy_plugin"]

Or in mypy.ini / setup.cfg:

[mypy]
plugins = param.mypy_plugin

The plugin is needed because param uses a metaclass __setattr__ to route class-level parameter assignment through the descriptor protocol. Without the plugin, mypy does not recognize this pattern (mypy #9758) and rejects valid code like:

class MyModel(param.Parameterized):
    flag = param.Boolean(default=False)

MyModel.flag = True  # mypy error without the plugin

With the plugin enabled, mypy correctly understands that the assignment sets the parameter’s default value and type-checks it against the parameter’s value type.

basedpyright#

basedpyright is a community fork of pyright with stricter default rules. Param does not officially support basedpyright-specific type rules, but it works fine if you suppress one false positive.

basedpyright enables reportUnannotatedClassAttribute by default, which warns on any class attribute without an explicit type annotation in non-@final classes.

This rule does not apply well to Param because the Parameter descriptors already encode the full type contract via __get__/__set__ overloads, making the warnings false positives. Marking Parameterized classes as @final would break Param’s fundamental design pattern since these classes are routinely subclassed. Adding redundant type annotations (e.g. title: str = param.String()) creates a maintenance burden and a potential source-of-truth mismatch between the annotation and the Parameter’s actual type (consider allow_None=True being added later without updating the annotation).

The Recommended fix is to suppress the rule project-wide. In pyproject.toml:

[tool.basedpyright]
reportUnannotatedClassAttribute = "none"

Or in pyrightconfig.json:

{
  "reportUnannotatedClassAttribute": "none"
}

Note that this also suppresses the warning for non-Parameterized classes in your project. If you want the rule active elsewhere, you can disable it per-file by adding a comment at the top of files that use Param:

# pyright: reportUnannotatedClassAttribute=none

To isolate basedpyright-specific rules when debugging type errors, you can temporarily set the following setting:

[tool.basedpyright]
typeCheckingMode = "standard"
reportAny = "none"
reportExplicitAny = "none"
reportUnreachable = "none"
reportUnusedParameter = "none"
reportIgnoreCommentWithoutRule = "none"