Skip to content

Conversation

@maximlt
Copy link
Member

@maximlt maximlt commented Oct 30, 2025

Resolves #508

In the simple and likely most common case, default_factory is set with a callable that is called internally on instance creation without any argument. This is useful for example to set a UUID on each new instance, or a created_at field:

import param
import datetime
import functools
import uuid

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.UTC))

obj1 = Object()
print(obj1)
# Object(created_at=datetime.datetime(2025, 10, 30, 10, 33, 16, 470076, tzinfo=datetime.timezone.utc), name='Object00002', uuid='a466d2e9-5dca-4729-af5b-17cbcab49823')

My original motivation for default_factory was to find a way to re-implement the behavior of the name parameter all Parameterized classes have. To that end, the factory callable can be wrapped in a DefaultFactory instance, which will let Param know that it needs to call it with the three arguments cls, self, and parameter. Instantiating DefaultFactory with on_class=True enables the factory to also be called on class creation (in which case self is passed None).

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(Parameterized):
    debug_id = param.String(
        default_factory=param.parameterized.DefaultFactory(
            debug_id,
            on_class=True,
        )
    )

class SubObject(Object):
    pass
image

On instance creation, the factory is called after the instance has been internally marked as initialized. I made that choice as:

  • The parameter (obtained directly from the parameter argument, or via self.param[<pname>]) is an instance-level parameter, and not class-level (only class-level parameters can be obtained before the class is initialized), which is less confusing and error-prone, I think
  • The factory has access to the values of all the other parameters

Reviewing testdefaultfactory.py is recommended to understand how this all works.

@codecov
Copy link

codecov bot commented Oct 30, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.21%. Comparing base (ca8755b) to head (96fa2cd).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1092      +/-   ##
==========================================
+ Coverage   89.15%   89.21%   +0.05%     
==========================================
  Files           9        9              
  Lines        4685     4709      +24     
==========================================
+ Hits         4177     4201      +24     
  Misses        508      508              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Member

@philippjfr philippjfr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation looks good. I left comments mostly with suggestions on wording, which you can accept or not, though be cautious because editing notebooks in GH is always error prone.

maximlt and others added 5 commits October 30, 2025 15:54
Co-authored-by: Philipp Rudiger <prudiger@anaconda.com>
Co-authored-by: Philipp Rudiger <prudiger@anaconda.com>
Co-authored-by: Philipp Rudiger <prudiger@anaconda.com>
Co-authored-by: Philipp Rudiger <prudiger@anaconda.com>
@maximlt
Copy link
Member Author

maximlt commented Oct 30, 2025

Thanks for your suggestions, I felt the wording was pretty mediocre but somehow refused to ask an LLM to help me, time to give up 🙃 It's much better now!

@maximlt
Copy link
Member Author

maximlt commented Nov 3, 2025

@philippjfr question on naming. I went for default_factory as that's how it's called in Python's dataclasses and Pydantic. On the other hand, attrs uses the shorter factory. Both options are supported by PEP 681 Data Class Transforms, if one day we manage to get this to work with Param.

I know that in the past I've sometimes been a bit confused with the default Parameter attribute. The class attribute value is equal to the default Parameter attribute value at the class level, but not at the instance level. This all makes sense, but I know that it used to trip me up. Is there a chance default_factory makes this a bit more confusing?

On my end, I don't have any strong opinion on naming this, and I'm fine with default_factory as it's the most commonly used (assuming pydantic and dataclasses have more users than attrs basically), but I'm curious to heard your thoughts on this.

@philippjfr
Copy link
Member

My vote is to stick with default_factory. I think it's clearer than the alternative.

@jbednar
Copy link
Member

jbednar commented Nov 4, 2025

I'm fine with default_factory, since one could imagine having factory functions for other things than the default value.

Copy link
Member

@jbednar jbednar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation looks good to me. Thanks for taking care of this! I hope my proposed wording works, but if not, please adapt it to be more accurate but do try to be more explicit in the way I'm suggesting. I see that it can be tricky to describe in the case of on_class=True!

"Each Parameter type can define additional behavior and associated metadata, but the metadata supported for all Parameter types includes:\n",
"\n",
"- **default**: Default value for this parameter at the class level, which will also be the value at the Parameterized instance level if it hasn't been set separately on the instance.\n",
"- **default_factory**: Optional callable to generate the attribute value. The callable can either be passed directly (in which case it is called with 0 arguments), or can be wrapped in a `DefaultFactory` for advanced use cases (in which case the factory is called with the arguments `cls`, `self` and `parameter`). `default_factory` takes precedence over `default` when set. On instance creation, the factory is called once the Parameterized instance is initialized.\n",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"the attribute value" seems confusing here, as it doesn't have any apparent linkage to the default metadata item defined just previously. Maybe:

Suggested change
"- **default_factory**: Optional callable to generate the attribute value. The callable can either be passed directly (in which case it is called with 0 arguments), or can be wrapped in a `DefaultFactory` for advanced use cases (in which case the factory is called with the arguments `cls`, `self` and `parameter`). `default_factory` takes precedence over `default` when set. On instance creation, the factory is called once the Parameterized instance is initialized.\n",
"- **default_factory**: Optional callable to generate the `default` value for this parameter when an instance of this class is created. The callable can either be passed directly (in which case it is called with 0 arguments), or can be wrapped in a `DefaultFactory` for advanced use cases (in which case the factory is called with the arguments `cls`, `self` and `parameter`). `default_factory` takes precedence over `default` when set. On instance creation, the factory is called once the Parameterized instance is initialized.\n",

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not quite correct as the factory is not called when the Parameter instance is created, but when the Parameterized instance is created.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 96fa2cd

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops; that's what I meant!

"The `default_factory` can be any callable that can be invoked without requiring positional arguments (e.g. `def foo(): ...`, `def foo(*args): ...`, `def foo(**kwargs): ...`, etc.). In addition, the callable can be wrapped as an instance of `param.parameterized.DefaultFactory`, in which case it is called with the three arguments `cls`, `self`, and `parameter`:\n",
"\n",
"- When an instance is created, the callable receives the `Parameterized` class as `cls`, the instance itself as `self`, and the instance-level `Parameter` object as `parameter`.\n",
"- During class creation (typically when a module defining `Parameterized` classes is imported), and only if the `DefaultFactory` is initialized with `on_class=True` (default is `False`), the callable is instead passed the `Parameterized` class as `cls`, `None` for `self` (since no instance exists yet), and the class-level `Parameter` object as `parameter`. This is the only case where the callable can influence the *class-level* attribute value.\n",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explicitly state what "This" is?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9a31616

maximlt and others added 3 commits November 4, 2025 14:12
Co-authored-by: James A. Bednar <jbednar@continuum.io>
@maximlt maximlt merged commit e00e76b into main Nov 4, 2025
15 checks passed
@maximlt maximlt deleted the default_factory branch November 4, 2025 14:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

factory function for Parameter instead of default value + instantiate=True

4 participants