Param
Param is a zero-dependency Python library that provides two main features:
- Easily create classes with rich, declarative attributes -
Parameter objects - that include extended metadata for various purposes such as runtime type and range validation, documentation strings, default values or factories, nullability, etc. In this sense, Param is conceptually similar to libraries like Pydantic, Python's dataclasses, or Traitlets.
- A suite of expressive and composable APIs for reactive programming, enabling automatic updates on attribute changes, and declaring complex reactive dependencies and expressions that can be introspected by other frameworks to implement their own reactive workflows.
This combination of rich attributes and reactive APIs makes Param a solid foundation for constructing user interfaces, graphical applications, and responsive systems where data integrity and automatic synchronization are paramount. In fact, Param serves as the backbone of HoloViz’s Panel and HoloViews libraries, powering their rich interactivity and data-driven workflows.
Here is a very simple example showing both features at play. We declare a UserForm class with three parameters: age as an Integer parameter and and name as a String parameter for user data, and submit as an Event parameter to simulate a button in a user interface. We also declare that the save_user_to_db method should be called automatically when the value of the submit attribute changes.
import param
class UserForm(param.Parameterized):
age = param.Integer(bounds=(0, None), doc='User age')
name = param.String(doc='User name')
submit = param.Event()
@param.depends('submit', watch=True)
def save_user_to_db(self):
print(f'Saving user to db: name={self.name}, age={self.age}')
...
user = UserForm(name='Bob', age=25)
user.submit = True
Enjoying Param? Show your support with a Github star to help others discover it too! ⭐️
| Downloads |  |
| Build Status |  |
| Coverage |  |
| Latest dev release |  |
| Latest release |  |
| Python |  |
| Docs |  |
| Binder |  |
| Support |  |
Rich class attributes for runtime validation and more
Param lets you create classes and declare facts about each of their attributes through rich Parameter objects. Once you have done that, Param can handle runtime attribute validation (type checking, range validation, etc.) and more (documentation, serialization, etc.). Let's see how to use Parameter objects with a simple example, a Processor class that has three attributes.
import param
class Processor(param.Parameterized):
retries = param.Integer(default=3, bounds=(0, 10), doc="Retry attempts.")
verbose = param.Boolean(default=False, doc="Emit progress messages.")
mode = param.Selector(
default="fast", objects=["fast", "accurate"],
doc="Execution strategy."
)
def run(self):
if self.verbose:
print(f"[{self.mode}] retry={self.retries}")
processor = Processor(verbose=True)
processor.run()
print(repr(processor))
try:
processor.retries = 42
except ValueError as e:
print(e)
print(processor.param['mode'].objects)
print(processor.param.mode.objects)
processor.param.update(mode='accurate', verbose=False)
print(processor.param.values())
Runtime attribute validation is a great feature that helps build defendable code bases! Alternative libraries, like Pydantic and others, excel at input validation, and if this is only what you need, you should probably look into them. Where Param shines is when you also need:
- Attributes that are also available at the class level, allowing to easily configure a hierarchy of classes and their instances.
- Parameters with rich metadata (
default, doc, label, bounds, etc.) that downstream tooling can inspect to build configuration UIs, CLIs, or documentation automatically.
- Parameterized subclasses that inherit Parameter metadata from their parents, and can selectively override certain attributes (e.g. overriding
default in a subclass).
Let's see this in action by extending the example above with a custom processor subclass:
class CustomProcessor(Processor):
retries = param.Integer(bounds=(0, 100))
verbose = param.Boolean(default=True)
print(CustomProcessor.verbose)
print(CustomProcessor.param['retries'].default)
print(CustomProcessor.param['retries'].bounds)
try:
CustomProcessor.retries = 200
except ValueError as e:
print(e)
cprocessor = CustomProcessor()
print(cprocessor.mode)
Processor.mode = 'accurate'
print(cprocessor.mode)
Reactive Programming
Param extends beyond rich class attributes with a suite of APIs for reactive programming. Let's do a quick tour!
We'll start with APIs that trigger side-effects only, which either have the noun watch in their name or are invoked with watch=True:
<parameterized_obj>.param.watch(fn, *parameters, ...): Low-level, imperative API to attach callbacks to parameter changes, the callback receives one or more rich Event objects.
@depends(*parameter_names, watch=True): In a Parameterized class, declare dependencies and automatically watch parameters for changes to call the decorated method.
bind(fn, *references, watch=True, **kwargs): Function binding with automatic references (parameters, bound functions, reactive expressions) watching and triggering on changes.
import param
def debug_event(event: param.parameterized.Event):
print(event)
class SideEffectExample(param.Parameterized):
a = param.String()
b = param.String()
c = param.String()
def __init__(self, **params):
super().__init__(**params)
self.param.watch(debug_event, 'a')
@param.depends('b', watch=True)
def print_b(self):
print(f"print_b: {self.b=}")
sfe = SideEffectExample()
sfe.a = 'foo'
sfe.b = 'bar'
def print_c(c):
print(f"print_c: {c=}")
param.bind(print_c, sfe.param.c, watch=True)
sfe.c = 'baz'
Let's continue the tour with what we'll call "reactive APIs". Contrary to the APIs presented above, in this group parameter updates do not immediately trigger side effects. Instead, these APIs let you declare relationships, dependencies, and expressions, which can be introspected by other frameworks to set up their own reactive workflows.
@depends(*parameter_names): In a Parameterized class, declare parameter dependencies by decorating a method.
bind(fn, *references, **kwargs): Create a bound function, that when called, will always use the current parameter/reference value. bind is essentially a reactive version of functools.partial.
rx(): Fluent API to create reactive expressions, which allow chaining and composing operations.
import param
class ReactiveExample(param.Parameterized):
x = param.Integer()
y = param.Integer()
@param.depends('x', 'y')
def sum(self):
return self.x + self.y
re = ReactiveExample()
def mul(a, b):
return a * b
bound_mul = param.bind(mul, re.param.x, re.param.y)
re.param.update(x=2, y=4)
bound_mul()
rx() is the highest-level reactive API Param offers. We'll show a simple example first, using literal values as input of three source reactive expressions, that we combine with simple arithmetic operations.
from param import rx
val1 = rx(0)
val2 = rx(0)
factor = rx(1)
res = (val1 + val2) * factor
print(res.rx.value)
val1.rx.value = 2
print(res.rx.value)
factor.rx.value = 10
print(res.rx.value)
The snippet below shows a slightly more complex example that reveals the power of reactive expressions.
import param
class RXExample(param.Parameterized):
val1 = param.String('foo')
val2 = param.String('bar')
example = RXExample()
print(example.param.val1.rx().title().rx.value)
example.val1 = 'fab'
print(example.param.val1.rx().title().rx.value)
cond1 = example.param.val1.rx().startswith('o')
print(cond1.rx.value)
cond2 = example.param.val2.rx().startswith('b')
print(cond1.rx.or_(cond2).rx.value)
A Parameter does not have to refer to a specific static value but can reference another object and update reactively when its value changes. We'll show a few examples of supported references (Parameters, bound functions, reactive expressions, etc.). Setting parameter values with references is an effective way to establish automatic one-way linking between a reference and a parameter (the reference being often driven by another parameter).
import param
class X(param.Parameterized):
source = param.Number()
class Y(param.Parameterized):
target = param.Number(allow_refs=True)
@param.depends('target', watch=True)
def watch_target(self):
print(f'y.target updated to {self.target}')
x = X(source=1)
y = Y(target=x.param['source'])
print(y.target)
x.source = 2
print(y.target)
y.target = param.bind(lambda x: x + 10, x.param['source'])
print(y.target)
x.source = 3
print(y.target)
y.target = x.param['source'].rx() * 20
print(y.target)
x.source = 5
print(y.target)
Support & Feedback
For more detail check out the HoloViz Community Guide.
Contributing
Check out the Contributing Guide.
License
Param is completely free and open-source. It is licensed under the BSD 3-Clause License.
The Param project is also very grateful for the sponsorship by the organizations and companies below: