Parameter Data

A ParamDB database stores parameter data. The abstract base class ParamData defines some core functionality for this data, including the last_updated, parent, and root properties. Internally, any subclasses of ParamData are automatically registered with ParamDB so that they can be loaded to and from JSON, which is how they are stored in the database.

All of the “Param” classes described on this page are subclasses of ParamData.

Important

Any data that is going to be stored in a ParamDB database must be a JSON serializable type (str, int, float, bool, None, dict, or list), a datetime, an astropy.units.Quantity, or an instance of a ParamData subclass. Otherwise, a TypeError will be raised when they are committed to the database.

Data Classes

A parameter data class is defined from the base class ParamDataclass. This custom class is automatically converted into a data class, meaning that class variables with type annotations become object properties and the corresponding __init__ function is generated. An example of a defining a custom parameter Data Class is shown below.

from paramdb import ParamDataclass

class CustomParam(ParamDataclass):
    value: float

custom_param = CustomParam(value=1.23)
print(custom_param)
CustomParam(value=1.23)

These properties can then be accessed and updated.

custom_param.value += 0.004
print(custom_param)
CustomParam(value=1.234)

The data class aspects of the subclass can be customized by passing keyword arguments when defining the custom class (the same arguments that would be passed to the @dataclass decorator), and by using the dataclass field function. The class arguments have the same default values as in @dataclass. An example of data class customization is shown below.

Note

The kw_only setting below only works in Python 3.10, but is useful for defining non-default arguments after those with default values (like in the example), especially when building up dataclasses through inheritance.

from dataclasses import field

class KeywordOnlyParam(ParamDataclass, kw_only=True):
    num_values: int = 0
    values: list[int] = field(default_factory=list)
    type: str

keyword_only_param = KeywordOnlyParam(type="example")
print(keyword_only_param)
KeywordOnlyParam(num_values=0, values=[], type='example')

Warning

For mutable default values, default_factory should generally be used (see the example above). See the Python data class documentation on mutable default values for more information.

Custom methods can also be added, including dynamic properties using the @property decorator. For example:

class ParamWithProperty(ParamDataclass):
    value: int

    @property
    def value_cubed(self) -> int:
        return self.value ** 3

param_with_property = ParamWithProperty(value=16)
print(param_with_property.value_cubed)
4096

Important

Since __init__ is generated for data classes, other initialization must be done using the __post_init__ function. Furthermore, since __post_init__ is used internally by ParamDataclass to perform initialization, always call the superclass’s __post_init__ first. For example:

class ParamCustomInit(ParamDataclass):
    def __post_init__(self) -> None:
        super().__post_init__()  # Always call the superclass __post_init__() first
        print("Initializing...")  # Replace with custom initialization code

param_custom_init = ParamCustomInit()
Initializing...

Parameter data track when any of their properties were last updated, and this value can be accessed by the read-only last_updated property. For example:

print(custom_param.last_updated)
2025-08-27 05:04:54.633045+00:00
import time

time.sleep(1)
custom_param.value = 4.56
print(custom_param.last_updated)
2025-08-27 05:04:55.660986+00:00

Last updated times for properties can also be accessed using by calling ParamData.child_last_updated() on the parent object. This is particularly useful for property values which are not ParamData. For example:

print(custom_param.child_last_updated("value"))
2025-08-27 05:04:55.660986+00:00

When parameter dataclasses are nested, updating a child also updates the last updated times of its parents. For example:

class NestedParam(ParamDataclass):
    value: float
    child_param: CustomParam

nested_param = NestedParam(value=1.23, child_param=CustomParam(value=4.56))
print(nested_param.last_updated)
2025-08-27 05:04:55.672804+00:00
time.sleep(1)
nested_param.child_param.value += 1
print(nested_param.last_updated)
2025-08-27 05:04:56.678174+00:00

You can access the parent of any parameter data using the ParamData.parent property. For example:

nested_param.child_param.parent is nested_param
True

Similarly, the root can be accessed via ParamData.root:

nested_param.child_param.root is nested_param
True

See Type Mixins for information on how to get the parent and root properties to work better with static type checkers.

Type Validation

If Pydantic is installed, parameter data classes will automatically be converted to Pydantic data classes, enabling runtime type validation. Some Pydantic configuration have modified defaults; see ParamDataclass for more information.

Pydantic type validation will enforce type hints at runtime by raising an exception. For example:

import pydantic

try:
    CustomParam(value="123")
except pydantic.ValidationError as exception:
    print(exception)
1 validation error for CustomParam
value
  Input should be a valid number [type=float_type, input_value='123', input_type=str]
    For further information visit https://errors.pydantic.dev/2.8/v/float_type

Type validation can be disabled for a particular parameter data class (and its subclasses) using the class keyword argument type_validation:

class NoTypeValidationParam(CustomParam, type_validation=False):
    pass

NoTypeValidationParam(value="123")
NoTypeValidationParam(value='123')

Files

ParamFile is an abstract base class that stores the path to a file. The data in the file can then be loaded by accessing ParamFile.data and updated using ParamFile.update_data(). In order to use ParamFile, it must be subclassed and the functions ParamFile._save_data() and ParamFile._load_data() must be defined.

Pandas DataFrames

One class that implements these functions is ParamDataFrame for saving and retrieving Pandas DataFrames. For example:

import pandas as pd
from paramdb import ParamDataFrame

data_frame = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=["col1", "col2", "col3"])
param_data_frame = ParamDataFrame("data.csv", data_frame)
param_data_frame.data
col1 col2 col3
0 1 2 3
1 4 5 6

Collections

Ordinary lists and dictionaries can be used within parameter data; however, any parameter data objects they contain will not have a last updated time or a parent object. Therefore, it is not recommended to use ordinary lists and dictionaries to store parameter data. Instead, ParamList and ParamDict can be used.

Parameter Lists

ParamList implements the abstract base class MutableSequence from collections.abc, so it behaves similarly to a list. It is also a subclass of ParamData, so the last updated, parent, and root properties will work properly. For example:

from paramdb import ParamList

param_list = ParamList([1, 2, 3])
print(param_list.child_last_updated(1))
2025-08-27 05:04:56.721191+00:00

Parameter Dictionaries

Similarly, ParamDict implements MutableMapping from collections.abc, so it behaves similarly to a dictionary. Additionally, items can be accessed via dot notation in addition to index brackets. For example:

from paramdb import ParamDict

param_dict = ParamDict(p1=1.23, p2=4.56, p3=7.89)
print(param_dict.child_last_updated("p2"))
2025-08-27 05:04:56.725699+00:00

Parameter collections can also be subclassed to provide custom functionality. For example:

class CustomDict(ParamDict[float]):
    @property
    def total(self) -> float:
        return sum(self.values())

custom_dict = CustomDict(param_dict)
print(custom_dict.total)
13.68

Type Mixins

The return type hint for ParamData.parent and ParamData.root is ParamData. Since the parent and root objects can change, it is not possible to automatically infer a more specific type for the parent or root. However, a type hint can be given using the ParentType and RootType mixins. For example:

from paramdb import ParentType

class ParentParam(ParamDataclass):
    child_param: ChildParam

class ChildParam(ParamDataclass, ParentType[ParentParam]):
    value: float

parent_param = ParentParam(child_param=ChildParam(value=1.23))

This does nothing to the functionality, but static type checkers will now know that parent_param.child_param.parent in the example above is a ParentParam object.