Param 2.0 release

release
param
Release announcement for Param 2.0
Author

Maxime Liquet

Published

December 22, 2023

TL;DR

We are very happy to announce the release of Param 2.0, a Python library that lets you write classes whose attributes, a.k.a. Parameters, are dynamically validated and whose updates can trigger actions you register. This major release includes:

  • improved inheritance of Parameter attributes, now applying even when the value is None, eliminating a previously very confusing limitation
  • extensive clean up of the Parameterized namespace, with public methods (deprecated for a while) moved to the .param namespace and reduced and consolidated private members
  • introduction of the new allow_refs and nested_refs Parameter attributes to allow linking a Parameter value from a reference (e.g. other Parameter value, param.depends function/method)
  • enhancement of the objects attribute of Selector Parameters, so it’s easier to update/access whether it’s been instantiated from a list or from a dictionary
  • introduction of a rich HTML representation for Parameterized instances and classes, which is automatically displayed in notebooks
  • an [experimental] preview of reactive expressions, which form a new declarative and reactive API for writing dynamic code without needing explicit callbacks or dependency declarations.

🌟 An easy way to support Param is to give it a star on Github! 🌟

Find all the changes brought by Param 2.0 in the release notes and see how to migrate your code from Param 1.x to 2.0 in the upgrade guide.

What is Param?

Param is a Python library that lets you write classes whose parameters, are equipped with metadata and are dynamically validated, which simplifies writing classes with a well-defined and defendable API. Additionally, a parameter can be watched to register a callback that is triggered when the parameter value changes.

Here’s a simple example usage of Param, where we create a User class that inherits from param.Parameterized and declares a couple of parameters, including age that can only be a positive integer. We also declare that the callback method submit_data should be called whenever the value of the three declared parameters is updated.

import param

class User(param.Parameterized):
    age = param.Integer(bounds=(0, None), doc='User age')
    fullname = param.String(doc='User full name', constant=True)
    country = param.Selector(default='en', objects=['en', 'fr', 'de'], doc='User country')

    @param.depends('age', 'fullname', 'country', watch=True)
    def submit_data(self):
        print(f'Submit data: {self.age=}, {self.fullname=}, {self.country=}')

user = User(age=33, fullname='John Doe')
user.age, user.fullname, user.country
(33, 'John Doe', 'en')

An error is raised if we try to set the value of one of these parameters to a value that doesn’t satisfy the parameter definition:

with param.exceptions_summarized():
    user.country = 'es'
ValueError: Selector parameter 'User.country' does not accept 'es'; valid options include: '[en, fr, de]'

When we update the value of the parameters of this instance, the callback submit_data gets automatically called:

user.age += 1
Submit data: self.age=34, self.fullname='John Doe', self.country='en'
user.country = 'fr'
Submit data: self.age=34, self.fullname='John Doe', self.country='fr'

These two features, dynamic parameter validation and and callback handling, makes Param an excellent library for supporting GUI code bases. Param is indeed the backbone of the HoloViz libraries HoloViews and Panel, with its integration in Panel being pushed to the extent that its users can easily convert Param objects into visual components. For instance, we can easily create a form from the user object, and to make it a real application we’d just have to update the callback to send data to an API or a database.

import panel as pn
pn.extension()
pn.Param(user)

Param 2.0!

Param 2.0 is a major new release available for Python 3.8 and above, significantly streamlining, simplifying, and improving the Param API. We would like to thank @minimav, @sdrobert, @droumis, @Hoxbro, @jbednar, @maximlt, @jlstevens and @philippjfr for their contributions. We would also like to thank @ceball, who made the first plans for Param 2.0 quite a few years ago, and we are glad to be delivering on them at last!

Join us on Github to contribute to Param directly or come chat with us on our Forum or Discord.

Improved Parameter attributes inheritance

A Parameter object has attributes like default, doc, bounds, etc. When redefining a Parameter in a subclass, attributes not specified in the subclass are meant to be inherited from the superclass(es). Parameter attribute inheritance is not a new feature; it’s a core design of Param. However, throughout the Param 1.x series, inheritance was arguably broken in subtle ways! The issues traced back to using None as the specific sentinel value for allowing inheritance, when None is also often a valid Parameter value. Let’s have a look at two cases that have been fixed in Param 2.0.

In this example, in subclass B, we want to re-set the default value of x to None and to narrow the bounds of y:

class A(param.Parameterized):

    x = param.Number(default=5.0)
    y = param.Number(default=5.0, bounds=(-10, 10))

class B(A):

    x = param.Number(default=None)
    y = param.Number(bounds=(0, 10))

b = B()
print(b.x, b.y)
# Param < 2: 5.0,  0.0   :(
# Param 2.0: None, 5.0   :)

As you can see for versions preceding Param 2.0, for x the explicit None value we provided in B was discarded entirely because None was interpreted incorrectly as “inherit from the parent class” when provided for an attribute value. Confusingly, None was treated differently for default values, where in y the default value of the Number Parameter (0.0) was used for B.x instead of the value of 5.0 explicitly declared in A. Param 2.0 now inherits values consistently from superclasses, getting None for x and 5.0 for y.

These are just two of the most common cases where parameter attribute inheritance changes can affect your code, but there are many other cases, affecting any attribute where None is a legal value. The new behavior should be much more predictable and intuitive, avoiding subtle cases where your code would previously have behaved inappropriately without necessarily having any obvious error. Fixing this was already enough to be worth the major bump to Param 2.0!

Cleaner Parameterized namespace

Parameterized classes are created by inheriting from param.Parameterized and as such your Parameterized classes will inherit from all of its members. In Param 2.0 we have removed many (many!) methods from this namespace that were deprecated a long ago and moved them to the .param namespace. We’ve also limited the number of private members to a minimum to minimize the risk of collision with members you’d like to create. To be able to defend a reasonably small API surface we require you not to override:

  • .param: namespace from which you can access the Parameter objects and many methods that used to be on the Parameterized namespace
  • ._param__parameters and ._param__private: private namespaces required for internal reasons
  • .name: Parameterized classes are equipped by default with a name String Parameter, though in Param 2.x you can now finally redefine it if you want it to behave differently!

Improved Selector objects attribute

The objects attribute of a Selector Parameter was previously highly confusing, because it accepted either a dictionary or a list for initialization but then was accessible only as a list, making it difficult to watch or update the objects. There is now a ListProxy wrapper around Selector.objects (with forward and backward compatibility) to easily update objects and watch objects updates.

class SelectorObjectsDemo(param.Parameterized):
    slist = param.Selector(objects=[1, 2, 3])
    sdict = param.Selector(objects=dict(a=1, b=2, c=3))

    @param.depends('slist:objects', watch=True)
    def updated_list_objects(self):
        print('Updating slist.objects:', self.param['slist'].objects)

    @param.depends('sdict:objects', watch=True)
    def updated_dict_objects(self):
        print('Updating sdict.objects:', self.param['sdict'].objects)

sdemo = SelectorObjectsDemo()
sdemo.param['slist'].objects.append(4)
Updating slist.objects: [1, 2, 3, 4]
# Now supported thanks to the ListProxy wrapper
sdemo.param.sdict.objects['d'] = 5
Updating sdict.objects: [1, 2, 3, 5]

Rich HTML representation

Parameterized classes and instances now have a rich HTML representation that is displayed automatically in a Jupyter/IPython notebook. For a class or instance p, just return p.param in a notebook cell to see a table of all the Parameters of the class/instance, their state, type, and range, plus the docstring on hover. It is likely we will improve the content and design of this repr based on feedback, so please let us know what you think!

user.param
User()
Name Value Type Range

age

34 Integer >=0

country

'fr' Selector 'en', 'fr', 'de'

fullname

'John Doe' String constant

name

'User00002' String nullable constant

Experimental new Reactive Expressions (rx)

Param is widely used for building web apps in the HoloViz ecosystem, where packages have added various mechanisms for dynamic updates (e.g. pn.bind and pn.depends in Panel, and .interactive in hvPlot). These mechanisms were already built on Param and can be used far more widely than just in those packages, so that functionality has now been generalized, streamlined, and moved into Param. Nearly any Python expression can now be made reactive with param.rx(), at which point it will collect and be able to replay any operations (e.g. method calls) performed on them. This reactive programming approach lets you take just about any existing Python workflow and replace attributes with widgets or other reactive values, creating an app with fine-grained user control without having to design callbacks, event handlers, or any other complex logic! rx support is still experimental while we get feedback about the API, packaging, and documentation, but it’s fully ready to try out and give us suggestions!

We’ll just show you a glimpse of what rx is capable of, and then you can head over to the documentation to learn more about it. Expect also a follow-up blog post that will dive more into it, describe how it is used throughout the HoloViz ecosystem (Panel in particular), and explain how it could be adopted more widely by other ecosystems and frameworks.

We start by importing rx and then simply create a reactive object by wrapping a Python object with rx(<obj>).

from param import rx

i = rx(1)
repr(i)
'<param.reactive.rx object at 0x132ba3c10>'

The object returned and stored on the variable i acts as a proxy of the wrapped object 1, i.e. we can use it as if it was the wrapped object.

i + 10
i * 10

When we set up a reactive expression j = i + 1, where i has been made reactive, any change to i will automatically trigger an update in j, eliminating the need for manual event handling. No more callbacks, Param takes care of recording the whole pipeline of operations applied to the root reactive object and replays it automatically when it’s updated, to finally update the value of the output. If you are executing this code in a live notebook, uncomment the following cell to see the previous cells update automatically with the new value. (Or just watch the GIF below to see how it works!)

#i.rx.value *=2

Simple Param reactive expression example

And more

Find all the changes brought by Param 2.0 in the release notes and see how to migrate your code from Param 1.x to 2.0 in the upgrade guide.

Back to top