Upgrade guide#

Welcome to the Upgrade Guide for Param! When we make backward-incompatible changes, we will provide a detailed guide here on how you can update your code to be compatible with the latest changes.

Version 2.0#

Breaking changes#

Parameter attributes inheritance#

Parameters have 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. Inheritance is not a new feature; it’s a core design of Param. You can see in the example below that the doc attribute of the x Parameter on the class B has been inherited from A:

class A(param.Parameterized):

    x = param.Number(default=5, doc='The value of x')

class B(A):

    x = param.Number(default=10)

print(B().param['x'].doc)
# The value of x

However, throughout the Param 1.x series, inheritance was broken in some 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 examples that have been fixed in Param 2.0.

In this first example we want to re-set the default value of x to None in a subclass. As you can see, the explicit None value we provided in B was discarded entirely in versions before Param 2.0, while the new version now correctly re-assigns the value to None:

class A(param.Parameterized):

    x = param.Number(default=5.0)

class B(A):

    x = param.Number(default=None)

print(B().x)
# Param < 2: 5.0     :(
# Param 2.0: None    :)

In this second example we want to narrow the bounds x in the subclass B. Because no default value is specified for x in B, you might reasonably have expected the default value of 5.0 to be inherited from A. However, prior to Param 2.0, instead the default value of the Number Parameter (0.0) is used for B.x instead of what was declared in A:

class A(param.Parameterized):

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

class B(A):

    x = param.Number(bounds=(0, 10))

print(B().x)
# Param < 2: 0.0    :(
# Param 2.0: 5.0    :)

These are just two of the most common cases where 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!

Because of all our work on making sure that Parameter attributes are properly inherited, we’ve realized that you can now more easily end up with a Parameter whose state, i.e. the combination of all of its attribute values, is invalid according to the Parameter declarations. Therefore, once class inheritance has completed as your classes get defined, we have added validation that the Parameter’s default value is an allowed value. For now Param 2.0 is only emitting a warning when this validation fails, to avoid breaking your code on imports! If you do see these warnings, you should address them immediately by adding a legal default value or by relaxing one of the declared constraints on the value, because the warnings indicate a Parameter is in an invalid state. In the following example, the bounds of x are narrowed down in B without updating the default value that is inherited from A.x of -1, making the Parameter x on B in an invalid state:

class A(param.Parameterized):
    x = param.Number(default=-1, bounds=(-5, 5))

class B(A):
    x = param.Number(bounds=(0, 5))

# Param < 2: No warning and B().x == 0.0
# Param 2.0: Warning and B().x == -1 / ParamFutureWarning: Number parameter 'B.x' failed to validate its default value on class creation, this is going to raise an error in the future. The Parameter is defined with attributes which when combined with attributes inherited from its parent classes (A) make it invalid. Please fix the Parameter attributes.

Param doesn’t do any more validation that what was just explained when creating subclasses that override Parameters. For instance, it doesn’t programmatically enforce that a subclass should define a Parameter that is of the same type or a subtype of the Parameter defined in a super class. However, the best approach for you to follow is clearly to make sure that Parameters defined in subclasses satisfy the “is-a” relationship. I.e., for an instance b of subclass B and an instance a of parent class A, b should always be usable where a is. That’s the general rule for inheritance, and for Parameters it’s specifically that any parameter value accepted by B should be a valid setting for A. That way any code written for A, including code that handles the various values of any attribute of A, can be trusted to work properly with any instance of B. In more concrete terms, it means a Parameter of a subclass should not increase the range of accepted values, it should instead narrow it down, i.e. by specifying a subtype (e.g. param.Number to param.Integer) or restricting the range of accepted values (e.g. smaller bounds).

Defining custom Parameters#

To implement Parameter attribute inheritance correctly as described above, we have had to make some changes to how new Parameter classes are declared. While the custom Parameters you previously wrote for Param before 2.0 should continue to work as they always have, unless you update their definitions, they could continue to suffer from the attribute inheritance issues that have been fixed within Param itself. We now recommend custom Parameters to be written following the pattern we have adopted internally. In particular, we have:

  • introduced the param.parameterized.Undefined sentinel object that is used as the default value of the Parameter parameters in its __init__() method

  • introduced the _slot_defaults class attribute to declare the actual default values of the Parameter parameters, e.g. in the example below the default value of some_attribute is declared to be 10

  • leveraged @typing.overload to expose the real default values to static type checkers and IDEs like VSCode

All put together, this is now how Parameters are structured internally and how we recommend writing custom Parameter classes:

class CustomParameter(Parameter):

    __slots__ = ['some_attribute']

    _slot_defaults = _dict_update(Parameter._slot_defaults,
        default=None, some_attribute=10
    )

    @typing.overload
    def __init__(
        self,
        default=None, *, some_attribute=10,
        doc=None, label=None, precedence=None, instantiate=False, constant=False,
        readonly=False, pickle_default_value=True, allow_None=False, per_instance=True,
        allow_refs=False, nested_refs=False
    ):
        ...

    def __init__(self, default=Undefined, *, some_attribute=Undefined, **params):
        super().__init__(default=default, **params)
        self.some_attribute = some_attribute

    ...

To make Parameter attributes work as expected, we have overridden Parameter.__getattribute__ to detect whether the attribute value has been set to something that is not Undefined, and if not, to fall back to returning the default value declared in _slot_defaults:

print(CustomParameter(some_attribute=0).some_attribute)
# 0

print(CustomParameter().some_attribute)  # Fallback case
# 10

Default value of name accounted for#

Every Parameterized class has always been equipped with a name Parameter, which was originally to support using a Parameterized object in a GUI context that needs a text label, but is not always needed in other contexts. At the class level the name value is the class name, equivalent to <class>.__name__, and at the instance level the name value is automatically generated as a unique string, unless you set it in the constructor:

class A(param.Parameterized):
    x = param.Number()

print(A.name)
# A
print(A().name)
# A00002
print(A(name='some name').name)
# some name

However, sometimes you want to define your own default value for the name parameter, as in the following example. Before Param 2.0, any such default value was discarded at both the class and instance levels, but since 2.0 the provided name is now respected:

class Person(param.Parameterized):
    name = param.String(default='Eva')

print(Person.name)
# Param < 2: Person         :(
# Param 2.0: Eva            :)

print(Person().name)
# Param < 2: Person0000     :(
# Param 2.0: Eva            :)

Setting a non-Parameter attribute via the constructor#

Before Param 2.0, you could set an instance attribute that was not a Parameter via the constructor, though you would get a warning if you did so. This behavior was prone to let typos slip through your code, setting the wrong instance attribute but continuing to function. Starting from Param 2.0, providing an attribute value that is not a Parameter now raises a TypeError, similarly to when you call a Python callable with an incorrect parameter name:

class A(param.Parameterized):
    number = param.Number()

A(numbre=10)  # oops typo!
# Param < 2: WARNING:param.A00002: Setting non-parameter attribute numbre=10 using a mechanism intended only for parameters
# Param 2.0: TypeError: A.__init__() got an unexpected keyword argument 'numbre'

instance.param.watchers value changed#

instance.param.watchers no longer returns the transient watchers state, which was never very useful, and now instead returns what you would expect, i.e. a dictionary of the watchers set up on this instance. To access that dictionary previously, you would have had to access the private attribute instance._param_watchers, which is now deprecated and was never intended to be public API:

class A(param.Parameterized):
    x = param.Number()

    @param.depends('x', watch=True)
    def cb(self):
        pass

print(A().param.watchers)
# Param < 2: []
# Param 2.0: {'x': {'value': [Watcher(...)]}}

Clean-up of the Parameterized namespace#

To avoid having your Parameterized object namespaces polluted with internal Param details, the methods listed below have been removed from the Parameterized namespace (members available to any class that inherits from param.Parameterized). Most of the time, a replacement method is now available from the .param namespace instead:

  • _add_parameter: use instead param.add_parameter

  • params: use instead .param.values() or .param['param']

  • set_default: use instead for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")

  • print_param_defaults: for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")

  • set_param: use instead .param.update

  • set_dynamic_time_fn: use instead .param.set_dynamic_time_fn

  • get_param_values: use instead .param.values().items() (or .param.values() for the common case of dict(....param.get_param_values()))

  • force_new_dynamic_value: use instead .param.force_new_dynamic_value

  • get_value_generator: use instead .param.get_value_generator

  • inspect_value: use instead .param.inspect_value

  • _set_name: no replacement

  • __db_print: no replacement

  • warning: use instead .param.warning

  • message: use instead .param.log(param.MESSAGE, ...)

  • verbose: use instead .param.log(param.VERBOSE, ...)

  • debug: use instead .param.log(param.DEBUG, ...)

  • print_param_values: use instead for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")

  • defaults: use instead {k:v.default for k,v in p.param.objects().items()}

  • pprint and _pprint: use instead .param.pprint

  • script_repr: use instead param.parameterized.script_repr

  • state_pop: moved to .param._state_pop, it might be removed in a future version, let us know if you need it!

  • state_push: moved to .param._state_push, it might be removed in a future version, let us know if you need it!

Parameter clean-up#

  • Removed unused bounds slot from Boolean and Event

  • Removed private Parameter _internal_name slot

Additional removals#

  • Removed Time.next method that was only needed for Python 2

Deprecations#

Param 2.0 was originally meant to clean up even more Param’s API, except that we noticed that deprecating API in the release notes of previous versions was not enough to warn Param’s users (including ourselves!). Thus for 2.0 we decided to postpone removing some APIs, and this time properly warn Param’s users that the code is about to be deleted. We’ve also spent a lot of time looking at the code base while working on this major release, and noticed additional clean up that could be made in the future. Therefore when you upgrade to 2.0, you are likely to encounter quite a large number of deprecation warnings. Make sure you’re adapting your code as soon as you can whenever you notice one of the newly emitted warnings, because that code will not continue to be supported in later releases in the 2.x series. (Or you can pin your code to Param 1.x if you want to avoid the warnings and do not need any of the features or bugfixes from 2.0.)

Here is the complete list of deprecation warnings added in Param 2.0:

  • Parameter signature:

    • Instantiating parameters other than default with positional arguments is deprecated:

      • String('prefix-test', '^prefix'): deprecated!

      • String('prefix-test', regex='^prefix'): OK

      • String(default='prefix-test', regex='^prefix'): OK

    • For Selector parameters that accept objects as first positional argument, and ClassSelector parameters that accept class_ as first positional argument, passing any argument by position is deprecated:

      • Selector([1, 2]): deprecated!

      • Selector(objects=[1, 2]): OK

      • ClassSelector((str, int)): deprecated!

      • ClassSelector(class_=(str, int)): OK

      • It’s possible that in the future the signature of these two parameters will be aligned with the other parameters to accept default as first and only positional argument, but for now please use an explicit keyword so that your code will be compatible with all versions.

  • Parameter slots:

    • List._class: use instead item_type.

    • Number.set_hook: no replacement

  • param.__init__ module:

    • param.produce_value: no replacement

    • param.as_unicode: no replacement

    • param.is_ordered_dict: no replacement

    • param.is_ordered_dict: no replacement

    • param.hashable: no replacement

    • param.named_objs: no replacement

    • param.normalize_path: no replacement

    • param.abbreviate_paths: no replacement

  • param.parameterized module:

    • param.parameterized.all_equal: no replacement

    • param.parameterized.add_metaclass: no replacement

    • param.parameterized.batch_watch: use instead batch_call_watchers

    • param.parameterized.recursive_repr: no replacement

    • param.parameterized.overridable_property: no replacement

  • Parameterized .param namespace; many of these methods have been deprecated since version 1.12.0, but because the deprecation was announced only in the release notes and not with runtime warnings, meany users have failed to realize the methods were deprecated, sometimes even us included! All these methods now emit deprecation warnings when executed and are now clearly marked as deprecated in the API reference:

    • .param.set_default: use instead for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")

    • .param._add_parameter: use instead .param.add_parameter

    • .param.params: use instead .param.values() or .param['param']

    • .param.set_param: use instead .param.update

    • .param.get_param_values: use instead .param.values().items() (or .param.values() for the common case of dict(....param.get_param_values()))

    • .param.params_depended_on: use instead .param.method_dependencies

    • .param.defaults: use instead {k:v.default for k,v in p.param.objects().items()}

    • .param.print_param_defaults: use instead for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")

    • .param.print_param_values: use instead for k,v in p.param.objects().items(): print(f"{p.__class__.name}.{k}={repr(v.default)}")

    • .param.message: use instead .param.log(param.MESSAGE, ...)

    • .param.verbose: use instead .param.log(param.VERBOSE, ...)

    • .param.debug: use instead .param.log(param.DEBUG, ...)

  • Some methods now (correctly) warn that they are not safe to run in a Parameterized subclass constructor until you have called super().__init__(**params):

    • instance.param.objects(instance=True)

    • instance.param.trigger("<param_name>")

    • instance.param.watch(callback, "<param_name>")

  • Parameterized namespace:

    • instance._param_watchers (getter and setter): use instead the property inst.param.watchers