param

repository·main·Indexed 19 days ago

https://github.com/holoviz/param

A zero-dependency Python library providing declarative class attributes with runtime validation and a rich API for reactive programming. It enables the creation of robust, data-driven systems using Parameter objects for metadata (type, range, documentation) and tools like @param.depends, param.bind, and the rx() fluent API for building computational graphs and automatic state synchronization. Param serves as the backbone for HoloViz libraries such as Panel and HoloViews.

Tokens
57.6K
Snippets
178
Records
232
Agent score
67%

What's inside param

  1. What is Param and when to use it

    main

    Param is a zero-dependency Python library designed for two primary purposes:

    1. Rich, Declarative Attributes: It allows you to create classes with Parameter objects that include extended metadata. This metadata supports runtime type validation, range validation, documentation strings, default values/factories, and nullability. It is conceptually similar to Pydantic, Python's dataclasses, or Traitlets.
    2. Reactive Programming: It provides APIs for declaring complex reactive dependencies. This enables automatic updates when attributes change and allows other frameworks to introspect these dependencies to implement reactive workflows.

    Param is commonly used as a foundation for building user interfaces, graphical applications, and responsive systems. It serves as the core engine for HoloViz's Panel and HoloViews libraries.

  2. What is Param?

    main

    Param is a zero-dependency Python library designed for two primary purposes:

    1. Rich, Declarative Attributes: It allows you to create classes with Parameter objects that include extended metadata (type, range validation, documentation strings, default values, nullability, etc.). This is useful for building defendable codebases where attributes can be inspected by downstream tools to build UIs or CLIs.
    2. Reactive Programming: It provides APIs for declaring complex reactive dependencies and expressions. This enables automatic updates when attributes change, making it a foundation for responsive systems and user interfaces.

    Param is used as the backbone for HoloViz libraries like Panel and HoloViews.

  3. Implement reactive updates with @param.depends

    main

    You can make methods reactive by using the @param.depends decorator. By specifying watch=True, the decorated method will be automatically called whenever the specified parameter(s) change.

    This is useful for simulating UI events (using param.Event) or synchronizing state across different attributes.

    import param
    
    class UserForm(param.Parameterized):
        age = param.Integer(bounds=(0, None))
        name = param.String()
        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  # Triggers save_user_to_db
  4. Link parameters using references

    main

    A Parameter can be linked to another object (a reference) so that it updates automatically when that object changes.

    To allow a parameter to accept a reference (like another parameter, a bound function, or a reactive expression) instead of a static value, you must set allow_refs=True when defining the parameter.

    Supported References:

    • Another Parameter object.
    • A bound function created via param.bind().
    • A reactive expression created via rx() or .rx().

    When the source of the reference changes, the target parameter is automatically updated to the new resolved value.

    import param
    
    class X(param.Parameterized):
        source = param.Number()
    
    class Y(param.Parameterized):
        # Must set allow_refs=True to accept dynamic references
        target = param.Number(allow_refs=True)
    
    x = X(source=1)
    y = Y()
    
    # 1. Reference to another Parameter
    y.target = x.param['source']
    print(y.target)  # 1
    x.source = 2
    print(y.target)  # 2
    
    # 2. Reference to a bound function
    y.target = param.bind(lambda x: x + 10, x.param['source'])
    print(y.target)  # 12
    x.source = 3
    print(y.target)  # 13
    
    # 3. Reference to a reactive expression
    y.target = x.param['source'].rx() * 20
    print(y.target)  # 20
    x.source = 5
    print(y.target)  # 100
  5. Configure Parameterized classes via class attributes

    main

    Once parameters are declared, you can reconfigure the default values for all instances of a class by setting the attribute on the class itself. This allows for declarative configuration that can be driven by external sources like YAML, JSON, or CLI arguments.

    import param
    
    class A(param.Parameterized):
        title = param.String(default="sum")
    
    class B(A):
        a = param.Integer(2)
    
    # Reconfigure defaults at the class level
    A.title = "The sum is"
    B.a = 6
    
    o3 = B()
    # o3 will now use the new class-level defaults
    A.title = "The sum is"
    B.a = 6
    
    o3 = B()
  6. Build GUIs by separating domain logic from UI code

    main

    Param allows you to separate your domain-specific logic (the parameters your code needs) from your GUI code (the widgets used to control them).

    By declaring parameters within your Parameterized classes, you create a clear contract of what inputs your code accepts. GUI libraries like Panel can then inspect these parameters to automatically generate appropriate widgets. This ensures that your core logic remains robust and testable without a GUI, while your GUI remains decoupled from the underlying implementation details.

  7. Inherit and override Parameter metadata in subclasses

    main

    Subclasses of a Parameterized class inherit all parameter metadata from their parents. You can selectively override specific aspects of a parameter (like bounds or default) in the subclass while keeping other metadata intact.

    Metadata inheritance works at both the class level and the instance level. Class-level attribute changes in a parent class will affect all instances of the child class unless the child has overridden that specific parameter.

    import param
    
    class Processor(param.Parameterized):
        retries = param.Integer(default=3, bounds=(0, 10))
        verbose = param.Boolean(default=False)
    
    class CustomProcessor(Processor):
        # Overriding bounds and default, but inheriting other metadata
        retries = param.Integer(bounds=(0, 100))
        verbose = param.Boolean(default=True)
    
    # Check inheritance
    print(CustomProcessor.param['retries'].default)  # 3 (inherited)
    print(CustomProcessor.param['retries'].bounds)    # (0, 100) (overridden)
  8. Create a parameterized class with `Parameterized`

    main

    To add runtime validation and parameter management to a class, inherit from param.Parameterized. This allows you to define class attributes as parameters that can be watched, validated, and serialized. A Parameterized instance behaves like a standard Python class instance.

    import param
    
    class MyClass(param.Parameterized):
        x = param.Integer(default=3)
        y = param.Boolean(default=False)
    
    obj = MyClass(x=5)
    print(obj.x)  # 5
  9. Trigger side-effects with Param's imperative reactive APIs

    main

    Param provides APIs to trigger immediate side-effects when parameters change. These are identified by the watch keyword or the watch noun in the method name.

    1. obj.param.watch(fn, *parameters, ...): A low-level API to attach a callback to specific parameter changes. The callback receives one or more param.parameterized.Event objects containing metadata about the change (e.g., old, new, name, obj).
    2. @param.depends(*parameter_names, watch=True): A decorator for methods within a Parameterized class. When the specified parameters change, the decorated method is automatically called. Unlike the standard @param.depends, using watch=True ensures the method is triggered on change, but it does not receive an Event object as an argument.
    3. param.bind(fn, *references, watch=True, **kwargs): Binds a function to one or more references (parameters, bound functions, or reactive expressions). If watch=True, the function is automatically invoked whenever the referenced values change.
    import param
    
    def debug_event(event: param.parameterized.Event):
        print(event)
    
    class SideEffectExample(param.Parameterized):
        a = param.String()
        b = param.String()
    
        def __init__(self, **params):
            super().__init__(**params)
            # 1. Low-level watch
            self.param.watch(debug_event, 'a')
    
        # 2. Automatic dependency with watch=True
        @param.depends('b', watch=True)
        def print_b(self):
            print(f"print_b: {self.b=}")
    
    # 3. Function binding with watch=True
    def print_c(c):
        print(f"print_c: {c=}")
    
    sfe = SideEffectExample()
    param.bind(print_c, sfe.param.c, watch=True)
    
    sfe.a = 'foo'  # Triggers debug_event
    sfe.b = 'bar'  # Triggers print_b
    sfe.c = 'baz'  # Triggers print_c
  10. GUI toolkit support for Param

    main

    Param is designed to integrate with GUI interfaces by mapping Parameters to widgets. Currently, the primary support is through:

    • Panel: Provides support for Jupyter and Bokeh-server, mapping param.Parameter objects to widgets and param.Parameterized objects to sets of widgets.
  11. Use dynamic values and numbergen for parameter exploration

    main

    Param supports dynamic parameter values, making it ideal for simulations, machine learning, or parameter sweeps.

    Using Callables

    You can set a parameter to a callable (like a lambda). The callable is evaluated every time the parameter is accessed, but the code using the parameter sees only the returned value.

    import random
    import param
    
    class B(param.Parameterized):
        a = param.Integer(2)
        b = param.Integer(3)
        def __call__(self):
            return str(self.a + self.b)
    
    # 'a' will return a new random integer every time it is accessed
    o2 = B(a=lambda: random.randint(0, 5))
    print(o2.a)  # Returns an integer

    Using numbergen

    For more robust exploration, use the numbergen module. numbergen objects are picklable and support arithmetic operations, allowing you to build complex parameter sweeps or Monte Carlo simulations.

    import numbergen as ng
    
    # Combine choices and random distributions
    o3 = B(a=ng.Choice(choices=[2, 4, 6]),
           b=1 + 2 * ng.UniformRandomInt(ubound=3))
    
    # Accessing parameters will trigger the generation logic
    print(o3())
    import numbergen as ng
    
    o3 = B(a=ng.Choice(choices= [2, 4, 6]),
           b = 1 + 2 * ng.UniformRandomInt(ubound=3))
  12. Use Parameter subclasses for type-safe attributes

    main

    The param library provides a variety of specialized Parameter subclasses to define class attributes with built-in runtime validation, documentation, and serialization. Instead of using standard Python types, you use these subclasses to enforce constraints (like ranges or specific allowed values) and provide metadata.

    Commonly used subclasses include:

    • Basic Types: String, Bytes, Boolean, Integer, Number.
    • Collections: List, Dict, Tuple, Array, Series, DataFrame.
    • Specialized/UI: Color, Selector, FileSelector, Date, Path, XYCoordinates.
    • Logic/Reactive: Event, Dynamic, Callable, Action.