import datetime
import functools
import time
import uuid
import param
def uuid4():
return str(uuid.uuid4())
class Object(param.Parameterized):
uuid = param.String(default_factory=uuid4)
created_at = param.Date(default_factory=functools.partial(datetime.datetime.now, tz=datetime.timezone.utc))Rich parameters & reactive programming with Param: 2.3 release
default_factory and metadata, the new ParameterizedABC abstract base class, and more!
What is Param?
Param is a zero-dependency Python library that provides two main features:
- Easily create classes with rich, declarative attributes -
Parameterobjects - 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, 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 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 # => Saving user to db: name=Bob, age=25Param 2.3.0
We are very pleased to announce the 2.3.0 release of Param! This release includes:
- Improved docstrings (#992, #994, #997, #998)
- New Parameter attribute:
default_factory(#1092) - New Parameter attribute:
metadata(#1094) - Proper Parameterized abstract base classes with
ParameterizedABC(#1031) - Versioned website (#1079)
- Breaking changes (#1085)
As always, the full changelog is available on GitHub.
Many thanks to our new contributors @Azaya89, @Coderambling, and @ypsah, as well as to @MarcSkovMadsen, @hoxbro, @maximlt, @philippjfr, and all others for their continued maintenance and development efforts.
You can install Param with pip install param, or with conda install param (or conda install conda-forge::param) if you are using Anaconda.
🌟 An easy way to support Param is to give it a star on GitHub! 🌟
Improved docstrings
@MarcSkovMadsen led the effort to add many missing docstrings and update existing ones, adopting the NumPy docstring style and including useful code snippets. These improvements will benefit you in the Python REPL (with help or ? in a notebook), in your IDE, and in Param’s API documentation.
We are still missing improved docstrings for all Parameter subclasses; we would like to find a smart way to avoid repeating shared attribute definitions such as default, doc, etc.
New Parameter attribute: default_factory
Sometimes the default value for a Parameter must be computed dynamically when a Parameterized instance is created. Typical examples include auto-generating a UUID or a created_at timestamp. To cover this use case, we added the default_factory Parameter attribute (a name familiar to users of Pydantic, attrs, and Python’s dataclasses). In its simplest form, default_factory accepts a callable that will be invoked without any argument.
The default_factory callables above are invoked when an Object instance is created. The created_at and uuid values will differ between the two instances shown below.
obj1 = Object()
time.sleep(5)
obj2 = Object()
obj1Object(created_at=datetime.datetime(2025, 11, 21, 14, 35, 35, 18482, tzinfo=datetime.timezone.utc), name='Object00002', uuid='e814d302-8bbf-46e3-a262-920b98387fd8')
obj2Object(created_at=datetime.datetime(2025, 11, 21, 14, 35, 40, 22781, tzinfo=datetime.timezone.utc), name='Object00003', uuid='9c485982-65af-45c7-a5f5-88566763997c')
As shown in the repr above, objects include the usual name attribute, an identifier automatically generated by Param. We wanted to reproduce how name is generated using only default_factory. To that end, we introduced the DefaultFactory helper class, which wraps a callable that will be invoked with (cls, instance, parameter). DefaultFactory can be instantiated with on_class=True to request that the callable be run at class creation time.
debug_id_counter = 0
def debug_id(cls, self, parameter):
global debug_id_counter
if self:
# When the class value is overriden.
if getattr(cls, parameter.name) != cls.__name__:
return getattr(cls, parameter.name)
else:
name = f'{cls.__name__}{debug_id_counter:05d}'
debug_id_counter += 1
return name
else:
return cls.__name__
class Object(param.Parameterized):
debug_id = param.String(
default_factory=param.parameterized.DefaultFactory(
debug_id,
on_class=True,
)
)This mechanism lets us reproduce that advanced behavior and should be flexible enough for most use cases.
print(Object.debug_id, Object.name)Object Object
print(Object().debug_id, Object().debug_id, Object().debug_id)
print(Object().name, Object().name, Object().name)Object00000 Object00001 Object00002
Object00007 Object00008 Object00009
In other libraries (Pydantic, attrs, and Python’s dataclasses), default_factory is commonly used to set defaults for mutable containers such as list.
from dataclasses import dataclass, field
@dataclass
class D:
# l_default: list = [0, 1] # ValueError: mutable default <class 'list'> for field l_bad is not allowed: use default_factory
l_default_factory: list = field(default_factory=lambda: [0, 1])
D().l_default_factory[0, 1]
In Param, this use case was already supported via the instantiate Parameter attribute, which indicates that a deep copy of the default value should be made. This attribute defaults to False, except for Parameters that represent mutable containers such as List or Dict. We haven’t decided whether instantiate should be deprecated now that default_factory exists, so for now you can use whichever option you prefer.
class P(param.Parameterized):
l_instantiate = param.List(default=[0, 1])
l_default_factory = param.List(default_factory=lambda: [0, 1])Visit the section about default_factory in the Parameters user guide.
New Parameter attribute: metadata
Have you ever wanted to enrich your Parameters with additional arbitrary metadata? Perhaps you’ve previously used a Parameter attribute like precedence to store ad‑hoc metadata. You can now store arbitrary mappings in the metadata Parameter attribute.
extra_info = {'library': {'config': True}}
class P(param.Parameterized):
s = param.String(metadata=extra_info)
P.param['s'].metadata{'library': {'config': True}}
P().param['s'].metadata{'library': {'config': True}}
Note that Param does not use the metadata attribute internally. A common use case is for third-party libraries to provide richer integrations. For example, Panel could inspect a Parameter’s metadata for custom widget configuration when building automatic UIs. This idea is being discussed in this issue and this one.
import panel as pn
pn.extension()
class MyPanelView(param.Parameterized):
percentage = param.Number(default=0, bounds=(0, 100), metadata={'panel': {'width': 100}})
pn.Param(MyPanelView())Visit the section about metadata in the Parameters user guide.
Proper Parameterized abstract base classes with ParameterizedABC
Abstract base classes (ABCs, more info in Python’s documentation) provide a formal way to define interfaces: they specify methods or properties that related classes must implement without prescribing how to implement them. Previously, declaring a Parameterized class as an ABC was not possible due to a metaclass conflict. This release introduces ParameterizedABC, which you can inherit instead of abc.ABC.
Here is an example that declares a ProcessorABC interface with an abstract run method that subclasses must implement:
import abc
class ProcessorABC(param.ParameterizedABC):
x = param.Number()
y = param.Number()
@abc.abstractmethod
def run(self): passSubclasses that do not implement the interface cannot be instantiated — this is the expected behavior of a Python ABC.
class BadProcessor(ProcessorABC):
def run_not_implemented(self): pass
with param.exceptions_summarized():
BadProcessor()TypeError: Can't instantiate abstract class BadProcessor without an implementation for abstract method 'run'
A valid subclass can be instantiated and used.
class GoodProcessor(ProcessorABC):
def run(self):
return self.x * self.y
GoodProcessor(x=2, y=4).run()8
Visit the section about ParameterizedABC in the Parameters user guide.
Versioned website
Earlier this year, we had the opportunity to add a version switcher to hvPlot’s website thanks to a NumFocus Small Development Grant. We reused that approach for Param’s site in this release, allowing you to switch between version 2.3.0 and the dev site.
Breaking changes
Many old methods of the .param namespace were soft-deprecated in version 0.12.0 (announced in the release notes), and properly deprecated with a warning message emitted when used in version 2.0.0, together with a collection of obsolete APIs. These APIs have been removed in this version, you can find the complete list in the release notes.
