Using Param¶
Correct patterns and common pitfalls for Param — the reactive parameter library that underpins Panel, HoloViews, and the HoloViz ecosystem.
Contents¶
- Parameterized Classes
- Reactive Dependencies (@param.depends)
- Dependent Parameters
- Parameter Types
- Reactive Expressions (rx)
- .watch() vs @param.depends vs .link()
- allow_refs
- Custom Parameter Types
Parameterized Classes¶
- Add
# pyright: reportAssignmentType=falseat the top — Param's descriptors conflict with static type checkers. - Add type annotations (
target: str = param.String(...)) for IDE autocomplete — Param doesn't enforce them at runtime. - Never use
nameas a parameter — reserved by Param for the instance name. self.param.param_keyis the Parameter object;self.param_keyis the current value. Useself.param.param_keywith.from_param()and pane constructors.
# pyright: reportAssignmentType=false
import param
class DataConfig(param.Parameterized):
source: str = param.Selector(default="CSV", objects=["CSV", "Parquet", "SQL"], doc="Data source type")
limit: int = param.Integer(default=1000, bounds=(1, 100_000), doc="Max rows to load")
filters: list = param.List(default=[], item_type=str, doc="Column filters to apply")
Reactive Dependencies (@param.depends)¶
- Without
watch=True: lazy, called only when something reads the result. Returns content. Withwatch=True: eager, fires every time the parameter changes. Use for side effects only. - Don't use
watch=Trueto update UI — causes flickering (thepanelskill covers this). on_init=Trueruns the method once at instantiation. Use withwatch=Trueto set initial state.- A method without
watch=Truemay run multiple times if multiple panes depend on it. Usewatch=Trueto update a parameter instead, then bind panes to that parameter.
import param
class Analysis(param.Parameterized):
query: str = param.String(default="SELECT *")
result = param.DataFrame()
@param.depends("result")
def summary(self):
if self.result is None:
return "No data loaded."
return f"**{len(self.result)} rows**, {len(self.result.columns)} columns"
@param.depends("query", watch=True, on_init=True)
def _run_query(self):
self.result = execute_query(self.query)
Dependent Parameters¶
- When updating
.objects, always check if the current value is still valid — reset it if not.
import param
class CountrySelector(param.Parameterized):
_countries = {
"Europe": ["France", "Germany", "Spain"],
"Asia": ["China", "Japan", "India"],
}
continent: str = param.Selector(default="Europe", objects=["Europe", "Asia"])
country: str = param.Selector(default="France", objects=["France", "Germany", "Spain"])
@param.depends("continent", watch=True, on_init=True)
def _update_countries(self):
countries = self._countries[self.continent]
self.param.country.objects = countries
if self.country not in countries:
self.country = countries[0]
Parameter Types¶
param.update()applies multiple changes atomically on one object — watchers fire once, not once per change. Also works as a context manager:with self.param.update(): self.x = 1; self.y = 2.- Use the most specific type (
param.IntegernotNumber,param.SelectornotString). Specificity drives widget selection in Panel's.from_param(). softboundssuggests a range for UI sliders without hard enforcement.stephints the increment.labeloverrides the display name.precedencecontrols ordering (lower = first).param.List(item_type=str)validates contents.param.Dictdoes not validate values.param.DataFrame()accepts pandas only. For Polars, useparam.Parameter().param.Event()resets toFalseafter firing watchers. Use withButton.from_param()+@param.depends(watch=True)when you need a declarative trigger.- Single source of truth: For navigation buttons (back/next), have handlers modify one shared parameter (e.g.,
active_step), then watch that parameter once. All UI state derives from one place. - Reassign, don't mutate: In-place operations (
+=,list.append(),dict.update()) don't trigger watchers. Always reassign:self.x = self.x + 1,self.items = self.items + [new],self.data = {**self.data, key: val}. default_factoryfor mutable/dynamic defaults — without it, all instances share the same object. Alternative:instantiate=True.- Param does not auto-coerce types (unlike Pydantic).
param.Integer(value="25")raisesValueError.
import uuid
import param
class TrackedItem(param.Parameterized):
id: str = param.String(default_factory=lambda: str(uuid.uuid4()))
tags: list = param.List(default=[], instantiate=True)
temperature: float = param.Number(
default=0.7, bounds=(0, 2), softbounds=(0, 1),
step=0.1, label="LLM Temperature", precedence=1,
)
submit: bool = param.Event(doc="Trigger processing")
config = DataConfig()
config.param.update(source="Parquet", limit=500) # one notification, not two
# Context manager form — useful when updating conditionally
with config.param.update():
config.source = "SQL"
config.limit = 200
# param.Event for buttons — use with @param.depends(watch=True)
class Wizard(param.Parameterized):
go_next = param.Event()
step = param.Integer(default=0)
@param.depends("go_next", watch=True)
def _on_go_next(self):
self.step = self.step + 1
# Then in Panel: pmui.Button.from_param(wizard.param.go_next, label="Continue")
Reactive Expressions (rx)¶
param.rx() / .rx() creates reactive expressions that automatically update when dependencies change. A lambda is to a Python function as rx is to pn.bind. Use rx instead of lambdas and pn.bind for cleaner, more declarative code.
import panel as pn
# ✅ Concise rx — replaces verbose pn.bind callback
button = pn.widgets.Button(name="Add " + select.param.value.rx())
# ❌ Verbose pn.bind equivalent
button = pn.widgets.Button(name=pn.bind(lambda x: f"Add {x}", select))
Core Operations¶
# Chain operations — indexing, slicing, methods all work
menu = MenuList(active=(0,))
step = menu.param.active.rx()[0] # extract first element reactively
# Conditional with rx.where (replaces if/else lambdas)
bar_color = toggle.param.value.rx.where("success", "warning")
# Boolean operations
visible = items.param.value.rx.len() > 0
hidden = toggle.param.value.rx.not_()
# Transform with rx.pipe — passes value as first arg
formatted = value.rx.pipe(format_func, extra_arg)
# Side effects with rx.watch (use sparingly)
expr.rx.watch(callback_func) # callback receives the value, not an event
Syncing Parameters¶
For syncing widget parameters to class parameters, use pn.bind(..., watch=True):
class Wizard(pn.viewable.Viewer):
active_step = param.Integer(default=0)
def __init__(self, **params):
self._menu = MenuList(items=[...], active=(0,))
pn.bind(self._on_menu_select, self._menu.param.active, watch=True)
super().__init__(**params)
def _on_menu_select(self, active):
if active and active[0] != self.active_step:
self.active_step = active[0]
Avoid allow_refs=True with rx binding (self.x = widget.param.value.rx()) when you also need to directly assign to that parameter — the binding breaks on direct assignment.
Gotchas¶
- Update via namespace:
expr.rx.value = new_value— direct assignmentexpr = new_valuebreaks reactivity - Accessor vs method:
.rx.valuegets current value,.rx()returns reactive expression for chaining - String concat:
"prefix " + widget.param.value.rx()works; for complex cases wrap literals withpn.rx("text") - Dict access for conflicts: Use
obj.param["objects"].rx.len()when parameter name conflicts with rx methods
.watch() vs @param.depends vs .link()¶
Prefer @param.depends("param_name", watch=True) over .watch() — it's declarative and avoids callback signatures with event arguments. Use .watch() only when reacting to parameters on an external instance or wiring watchers conditionally at runtime. If you need .old/.new for logging or undo, .watch() is appropriate.
For syncing parameters between objects, use .link() or .rx():
# Direct property sync — no callback needed
text_input.link(markdown_pane, value='object')
# Bind parameter to rx expression (requires allow_refs=True)
class Wizard(param.Parameterized):
active_step = param.Integer(default=0, allow_refs=True)
def __init__(self, **params):
self._menu = MenuList(items=[...], active=(0,))
super().__init__(**params)
self.active_step = self._menu.param.active.rx()[0] # menu -> step sync
For inline reactive expressions, use .rx() instead of lambdas:
# ✅ Preferred — rx for reactive string formatting
button = pn.widgets.Button(name="Add " + select.param.value.rx())
# ❌ Avoid — lambda callback
button = pn.widgets.Button(name=pn.bind(lambda x: f"Add {x}", select))
# ✅ Preferred — declarative, no event argument
class Wizard(param.Parameterized):
go_next = param.Event()
step = param.Integer(default=0)
@param.depends("go_next", watch=True)
def _on_go_next(self):
self.step = self.step + 1
# ❌ Avoid — imperative, requires event argument
def on_change(event):
print(f"{event.name}: {event.old} → {event.new}")
config.param.watch(on_change, ["source", "limit"])
allow_refs¶
allow_refs=True lets a parameter track another Parameter object, staying in sync automatically.
import param
class Source(param.Parameterized):
value: int = param.Integer(default=10)
class Consumer(param.Parameterized):
input_value: int = param.Integer(default=0, allow_refs=True)
source = Source()
consumer = Consumer(input_value=source.param.value)
source.value = 20
print(consumer.input_value) # 20
Custom Parameter Types¶
Subclass and override _validate_value. Always call super()._validate_value() first.