param
Param: Declarative Parameters
Create typed, validated class attributes with reactive programming support.
Hello World Example
# DO always add this to ignore pyright Parameter type annotation warnings
# pyright: reportAssignmentType=false
import param
class Greeter(param.Parameterized):
"""A greeting generator with history tracking."""
# DON'T use 'name' as parameter - it's reserved in Param
# DO add type annotations, defaults, and doc strings
target: str = param.String(default="World", doc="Name to greet")
greeting: str = param.Selector(default="Hello", objects=["Hello", "Hi", "Hey"])
count: int = param.Integer(default=1, bounds=(1, 10), doc="Repetitions")
history: list = param.List(default=[], doc="Greeting history")
# DO use @param.depends (watch=False) for computed values with no side effects
@param.depends("target", "greeting", "count")
def message(self) -> str:
"""Computed value - recalculates when dependencies change."""
return " ".join([f"{self.greeting}, {self.target}!"] * self.count)
# DO use @param.depends(watch=True) for side effects (state updates, I/O, etc.)
@param.depends("target", watch=True)
def _track_changes(self):
"""Side effect - automatically runs when target changes."""
self.history = self.history + [self.target]
# Usage
greeter = Greeter(target="Alice")
print(greeter.message()) # "Hello, Alice!"
greeter.target = "Bob"
print(greeter.history) # ["Bob"] - tracked the change
greeter.greeting = "Hi"
greeter.count = 2
print(greeter.message()) # "Hi, Bob! Hi, Bob!"
param.Parameterized (Production) vs param.rx/bind (Exploration)
Use param.Parameterized for production code. Use param.rx/param.bind only for notebook exploration:
Core Parameter Types
import datetime
import param
import numpy as np
import pandas as pd
class AllParameterTypes(param.Parameterized):
# Strings
name: str = param.String(default="unnamed", doc="Item name")
color: str = param.Color(default="#FF5733", doc="Hex color or named color")
# Numbers
count: int = param.Integer(default=10, bounds=(0, 1000))
rate: float = param.Number(default=0.5, bounds=(0.0, 1.0), step=0.1)
magnitude: float = param.Magnitude(default=0.5) # Always 0.0-1.0
# Boolean
enabled: bool = param.Boolean(default=True)
# Selectors
mode: str = param.Selector(default="auto", objects=["auto", "manual", "hybrid"])
tags: list = param.ListSelector(default=["a"], objects=["a", "b", "c"])
# Collections
items: list = param.List(default=[], item_type=str)
config: dict = param.Dict(default={})
data: np.ndarray = param.Array(default=np.array([]))
df: pd.DataFrame = param.DataFrame(default=pd.DataFrame())
# Dates
date: datetime.date = param.CalendarDate(default=datetime.date.today())
date_range: tuple = param.CalendarDateRange(default=None, doc="Optional date range")
value_range: tuple = param.Range(default=(0, 10), bounds=(0, 100))
# Files
input_file: str = param.Filename(default=None, doc="Input file path")
output_dir: str = param.Foldername(default=None, doc="Output directory")
# Actions and Events
submit: bool = param.Event(doc="Trigger processing")
callback: callable = param.Callable(default=None, doc="Processing function")
# Class instances
nested: param.Parameterized = param.ClassSelector(class_=param.Parameterized, default=None)
Parameter Metadata
import param
class DocumentedModel(param.Parameterized):
threshold = param.Number(
default=0.5,
bounds=(0, 1), # Hard limits - enforced
softbounds=(0.2, 0.8), # Suggested range for UIs
step=0.05, # Increment hint for UIs
doc="Classification threshold",
label="Threshold (%)", # Display name
precedence=1, # Order in UIs (lower = first)
constant=False, # If True, immutable after init
readonly=False, # If True, never settable by user
allow_None=False, # If True, None is valid
instantiate=False, # If True, deep copy default per instance
per_instance=True, # If True, separate Parameter object per instance
)
Dynamic Defaults with default_factory
import uuid
import datetime
import param
class TrackedItem(param.Parameterized):
id: str = param.String(default_factory=lambda: str(uuid.uuid4()))
created_at: datetime.datetime = param.Date(default_factory=datetime.datetime.now)
Dependencies with @param.depends
watch=False: Declare Dependencies for External Frameworks
import param
class DataView(param.Parameterized):
source: str = param.Selector(default="A", objects=["A", "B", "C"])
limit: int = param.Integer(default=10, bounds=(1, 100))
@param.depends("source", "limit")
def get_data(self) -> list:
"""Called by Panel/HoloViews when dependencies change."""
return [f"{self.source}_{i}" for i in range(self.limit)]
watch=True: Execute Side Effects Automatically
import param
class CountrySelector(param.Parameterized):
"""Dependent parameters pattern - updates country list when continent changes."""
_countries = {
"Europe": ["France", "Germany", "Spain"],
"Asia": ["China", "Japan", "India"],
"Americas": ["USA", "Brazil", "Canada"],
}
continent: str = param.Selector(default="Europe", objects=["Europe", "Asia", "Americas"])
country: str = param.Selector(default="France", objects=["France", "Germany", "Spain"])
@param.depends("continent", watch=True, on_init=True)
def _update_countries(self):
"""Automatically update country options when continent changes."""
countries = self._countries[self.continent]
self.param.country.objects = countries
if self.country not in countries:
self.country = countries[0]
on_init=True: Run on Instantiation
Always use on_init=True when a watcher should run during __init__:
@param.depends("config_path", watch=True, on_init=True)
def _load_config(self):
"""Load config on init AND when path changes."""
if self.config_path:
self.config = load_config(self.config_path)
Watchers (Low-level API)
import param
class WatcherExample(param.Parameterized):
value: int = param.Integer(default=0)
history: list = param.List(default=[])
def __init__(self, **params):
super().__init__(**params)
# Watch with callback receiving Event objects
self.param.watch(self._on_value_change, ["value"])
def _on_value_change(self, event):
"""event.old, event.new, event.name, event.obj available."""
self.history.append({"old": event.old, "new": event.new})
# Alternative: watch_values passes values as kwargs
model = WatcherExample()
model.param.watch_values(lambda value: print(f"Value: {value}"), ["value"])
Event Parameter for Triggers
import param
class Processor(param.Parameterized):
data: list = param.List(default=[])
process: bool = param.Event(doc="Click to process")
result: str = param.String(default="")
@param.depends("process", watch=True)
def _on_process(self):
"""Triggered when process event fires."""
self.result = f"Processed {len(self.data)} items"
processor = Processor(data=[1, 2, 3])
processor.process = True # Triggers _on_process, then resets to False
print(processor.result) # "Processed 3 items"
Parameter References (allow_refs)
import param
class Source(param.Parameterized):
value: int = param.Integer(default=10)
class Consumer(param.Parameterized):
# allow_refs=True lets this parameter reference another Parameter
input_value: int = param.Integer(default=0, allow_refs=True)
source = Source()
consumer = Consumer(input_value=source.param.value)
print(consumer.input_value) # 10
source.value = 20
print(consumer.input_value) # 20 - automatically updated
param.rx and param.bind (Exploration Only)
Use for notebooks and prototyping. Refactor to Parameterized for production:
import param
from param import rx
# param.rx - reactive values
data = rx([1, 2, 3])
doubled = data.rx.pipe(lambda d: [x * 2 for x in d]) # [2, 4, 6]
data.rx.value = [10, 20] # doubled becomes [20, 40]
# param.bind - bind function to parameters
class Config(param.Parameterized):
x: int = param.Integer(default=5)
y: int = param.Integer(default=10)
config = Config()
result = param.bind(lambda a, b: a * b, config.param.x, config.param.y)
print(result()) # 50
config.x = 7
print(result()) # 70
Testing Parameterized Classes
import pytest
import param
class Calculator(param.Parameterized):
a: float = param.Number(default=0)
b: float = param.Number(default=0)
operation: str = param.Selector(default="add", objects=["add", "multiply"])
@param.depends("a", "b", "operation")
def result(self) -> float:
return self.a + self.b if self.operation == "add" else self.a * self.b
def test_defaults():
calc = Calculator()
assert calc.a == 0 and calc.operation == "add"
def test_computed_values():
assert Calculator(a=5, b=3).result() == 8
assert Calculator(a=5, b=3, operation="multiply").result() == 15
def test_reactivity():
calc = Calculator(a=2, b=3)
assert calc.result() == 5
calc.a = 10
assert calc.result() == 13
def test_validation():
with pytest.raises(ValueError):
Calculator(a="not a number")
with pytest.raises(ValueError):
Calculator(operation="invalid")
Best Practices
DO
- Use Parameterized classes for production code
- Add type annotations for IDE support
- Add
# pyright: reportAssignmentType=falseat the top of files with type-annotated Parameters (Param's descriptors conflict with static type checkers) - Write pytest tests for all reactive methods
- Use
watch=Truefor side effects,watch=Falsefor computed values - Use
on_init=Truewhen watchers should run during initialization - Use
docparameter for documentation - Use
boundsfor numeric constraints
DON'T
- Use
nameas a parameter name - it's reserved (usetitle,label, etc.) - Use param.bind/rx for production code that needs testing
- Modify parameters inside their own
watch=Truecallbacks (causes loops) - Forget
on_init=Truewhen initialization logic depends on parameter values - Use mutable defaults without
instantiate=Trueordefault_factory
Common Patterns
Configuration Object
import param
class AppConfig(param.Parameterized):
debug: bool = param.Boolean(default=False)
log_level: str = param.Selector(default="INFO", objects=["DEBUG", "INFO", "WARNING", "ERROR"])
max_workers: int = param.Integer(default=4, bounds=(1, 32))
config = AppConfig()
config.param.update(debug=True, log_level="DEBUG") # Batch update, watchers called once
Environment Variable Defaults
import os
import param
env = os.environ.get
class AppSettings(param.Parameterized):
database_url = param.String(default=env("DATABASE_URL", ""), doc="Database connection URL")
secret_key = param.String(default=env("SECRET_KEY", ""), doc="Secret key for JWT tokens")
debug = param.Boolean(default=env("DEBUG", "false").lower() == "true")
allowed_hosts = param.List(default=env("ALLOWED_HOSTS", "localhost").split(","), item_type=str)
settings = AppSettings()
Note: Environment variables are read at class definition time. For dynamic reloading, read them in __init__ or use default_factory.
Batch Updates
# Update multiple parameters atomically
with config.param.update(debug=True, log_level="DEBUG"):
pass # Changes applied, watchers called once at end
# Or without context manager
config.param.update(debug=True, log_level="DEBUG")
Serialization
import param
class User(param.Parameterized):
age: int = param.Integer(default=0)
email: str = param.String(default="")
user = User(age=25, email="test@example.com")
user.param.values() # {'name': 'User00001', 'age': 25, 'email': '...'}
user.param.values(onlychanged=True) # {'age': 25, 'email': '...'}
json_str = user.param.serialize_parameters()
User.param.deserialize_parameters(json_str) # Returns dict for constructor
Pydantic Migration
Unlike Pydantic, Param does not auto-coerce types. Convert values explicitly:
import param
class ParamUser(param.Parameterized):
age: int = param.Integer()
ParamUser(age=25) # Works
ParamUser(age="25") # Raises ValueError - no coercion
# Convert when migrating from Pydantic
data = {"age": "25"}
ParamUser(age=int(data["age"]))
Cross-Field Validation
Use @param.depends(watch=True, on_init=True) to validate across multiple parameters:
import re
import param
class MinLengthString(param.String):
"""String with minimum length validation."""
__slots__ = ["min_length"]
def __init__(self, min_length=0, **params):
self.min_length = min_length
super().__init__(**params)
def _validate_value(self, val, allow_None):
super()._validate_value(val, allow_None)
if val and len(val) < self.min_length:
raise ValueError(f"Parameter {self.name!r} must be at least {self.min_length} characters.")
class EmailString(param.String):
"""String that must be a valid email format."""
def _validate_value(self, val, allow_None):
super()._validate_value(val, allow_None)
if val and not re.match(r"^[\w\.-]+@[\w\.-]+\.\w+$", val):
raise ValueError(f"Parameter {self.name!r} must be a valid email, not {val!r}.")
class UserRegistration(param.Parameterized):
"""User registration with cross-field validation."""
username: str = MinLengthString(min_length=3, doc="Username (min 3 characters)")
email: str = EmailString(doc="Email address")
password: str = param.String(doc="Password")
subscription_tier: str = param.Selector(default="free", objects=["free", "pro", "enterprise"])
@param.depends("password", "subscription_tier", watch=True, on_init=True)
def _validate_password(self):
"""Validate password complexity based on subscription tier."""
if not self.password:
return
if len(self.password) < 8:
raise ValueError("Password must be at least 8 characters")
if self.subscription_tier == "enterprise" and not re.search(r"[A-Z]", self.password):
raise ValueError("Enterprise accounts require uppercase letters")
# Usage - validation runs on init and parameter changes
user = UserRegistration(
username="alice", email="alice@example.com",
password="SecurePass123", subscription_tier="enterprise",
)
UserRegistration(username="bob", password="lowercase", subscription_tier="enterprise") # Raises ValueError
Custom Parameter Types
Subclass and override _validate_value for reusable parameters with custom validation:
import param
class EvenInteger(param.Integer):
"""Integer that must be even."""
def _validate_value(self, val, allow_None):
super()._validate_value(val, allow_None) # Always call parent first
if val is not None and val % 2 != 0:
raise ValueError(f"EvenInteger parameter {self.name!r} must be even, not {val!r}.")
class PositiveNumber(param.Number):
"""Number that must be > 0."""
def _validate_value(self, val, allow_None):
super()._validate_value(val, allow_None)
if val is not None and val <= 0:
raise ValueError(f"PositiveNumber parameter {self.name!r} must be positive, not {val!r}.")
class GridConfig(param.Parameterized):
rows: int = EvenInteger(default=4)
spacing: float = PositiveNumber(default=1.0)
config = GridConfig(rows=6, spacing=2.5)
config.rows = 5 # Raises ValueError: must be even
config.spacing = -1 # Raises ValueError: must be positive
Resources
More from marcskovmadsen/holoviz-mcp
panel
Best practices for developing tools, dashboards and interactive data apps with HoloViz Panel. Create reactive, component-based UIs with widgets, layouts, templates, and real-time updates. Use when developing interactive data exploration tools, dashboards, data apps, or any interactive Python web application. Supports file uploads, streaming data, multi-page apps, and integration with HoloViews, hvPlot, Pandas, Polars, DuckDB and the rest of the HoloViz and PyData ecosystems.
13panel-material-ui
Best practices for developing modern looking tools, dashboards and data apps using HoloViz Panel and Panel Material UI components.
10hvplot
Best practices for doing quick exploratory data analysis with minimal code and a Pandas .plot like API using HoloViews hvPlot.
7panel-holoviews
Best practices for integrating HoloViews and hvPlot visualizations into Panel applications. Use when embedding HoloViews/hvPlot plots in Panel panes, preserving zoom/pan state across data refreshes with DynamicMap, composing DynamicMap overlays without type errors, using HoloViews streams (Selection1D, RangeXY, Tap, BoundsXY, Pipe, Buffer) with Panel, cross-filtering with link_selections, making HoloViews plots responsive in Panel layouts, or wiring Panel widgets to Bokeh plot properties with jslink.
6holoviews
Best practices for developing advanced, interactive, and publication-quality data visualizations using HoloViz HoloViews
6panel-custom-components
Build custom Panel components using JSComponent (vanilla JS, web components), ReactComponent (React/JSX), AnyWidgetComponent (AnyWidget spec for cross-platform), or MaterialUIComponent (Material UI themed). Use when wrapping JS libraries, creating interactive widgets, or building themed components. Includes decision guide, best practices, DOs/DON'Ts, and Playwright UI testing patterns.
6