Skip to content

Simplify setting dynamic default values #866

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
valentincalomme opened this issue Oct 5, 2019 · 19 comments
Closed

Simplify setting dynamic default values #866

valentincalomme opened this issue Oct 5, 2019 · 19 comments

Comments

@valentincalomme
Copy link

valentincalomme commented Oct 5, 2019

Feature Request

Please complete:

  • OS: Windows
  • Python version 3.7.4 (default, Aug 9 2019, 18:34:13) [MSC v.1915 64 bit (AMD64)]
  • Pydantic version: 0.30

Feature Request

Currently, if I want to have a dynamic default value, it has to be done the following way:

from datetime import datetime

from pydantic import BaseModel, validator
from uuid import UUID, uuid4

class DemoModel(BaseModel):
    ts: datetime = None
    id: UUID = None

    @validator('id', pre=True, always=True)
    def set_id(cls, v):
        return v or uuid4()

    @validator('ts', pre=True, always=True)
    def set_ts_now(cls, v):
        return v or datetime.now()

This is fine for cases where actual validation has to be provided, however, in many cases like the one portrayed below, this leads to unnecessary code, especially if there are multiple dynamically set values. For instance, a timestamp and an id.

It would be great to be able to replace this usage pattern by a pydantic field called Dynamic for instance such that one can pass a callable as a default value. This would look like this:

from datetime import datetime
from uuid import UUID, uuid4

from pydantic import BaseModel, validator, Dynamic

class DemoModel(BaseModel):
    ts: Dynamic[datetime] = datetime.now
    id: Dynamic[UUID] = uuid4

I don't expect that this would need to modify any existing functionalities. Probably just a need to create some sort of wrapper.

@samuelcolvin
Copy link
Member

I'm hesitant about pseudo types, without reading the docs it's not clear what this does.

What we could do is just is the function as the default/initial value? Then unless the type of the field was Callable it would call the function to generate a value.

This would not be completely backwards, but I think it would be more intuitive.

@valentincalomme
Copy link
Author

valentincalomme commented Oct 6, 2019

This would work as well! The main idea would be to reduce the amount of boilerplate code required for this type of usage.

@dmontagu
Copy link
Contributor

dmontagu commented Oct 6, 2019

I think this might be better suited as a keyword argument to Schema (or Field now), or similar. That’s how this functionality is handled by both attrs and dataclasses.

@samuelcolvin
Copy link
Member

I think this might be better suited as a keyword argument to Schema (or Field now), or similar. That’s how this functionality is handled by both attrs and dataclasses.

agreed.

@Bobronium Bobronium mentioned this issue Feb 6, 2020
4 tasks
PrettyWood added a commit to PrettyWood/pydantic that referenced this issue Feb 7, 2020
PrettyWood added a commit to PrettyWood/pydantic that referenced this issue Feb 7, 2020
@samuelcolvin
Copy link
Member

#1210 will implement Field(default_factory=...).

For use of ts: datetime = datetime.now we should wait for v2.

@samuelcolvin samuelcolvin added this to the Version 2 milestone Feb 7, 2020
PrettyWood added a commit to PrettyWood/pydantic that referenced this issue Feb 8, 2020
PrettyWood added a commit to PrettyWood/pydantic that referenced this issue Feb 8, 2020
PrettyWood added a commit to PrettyWood/pydantic that referenced this issue Feb 10, 2020
PrettyWood added a commit to PrettyWood/pydantic that referenced this issue Feb 10, 2020
@mentalisttraceur
Copy link

Since the documentation still points here as the place to comment on the design of the default_factory feature:

I would like to suggest permitting the argument name factory as an alias for default_factory - it seems to me that it is self-evident in all cases that the only thing a field could have a factory for is for new instances of that value (and when are new instances needed? only when one is not provided, and we're providing a default).

@153957
Copy link

153957 commented Sep 6, 2021

The name default_factory was chosen to be consistent with native dataclasses.field & #1210.

@mentalisttraceur
Copy link

@153957

I didn't ask why it is the way it is.

I didn't argue against having default_factory.

I suggested also having factory, because it is more ergonomic.

@153957
Copy link

153957 commented Sep 8, 2021

Ok sure, I'm still 👎🏽 on adding aliases for arguments/keywords, no good reason to add the additional complexity and documentation.

from pydantic import Field

class ErgonomicField(Field):
    def __init__(*args, **kwargs):
        kwargs['default_factory'] = kwargs.pop('factory', None)
        super().__init__(*args, **kwargs)

@mentalisttraceur
Copy link

Yeah I don't love aliases either, but aliases are a good solution for backwards-compatibly migrating to a better name, and this is right in that middle ground of very minor but permeating improvement that is rarely worth doing on a case-by-case basis in a project, but would gradually ripple a slight benefit across many projects if done upstream.

@mentalisttraceur
Copy link

I do totally sympathize with the resistance. I do the same thing when people bring suggested changes to any of my repos - I don't like messing up and growing my public interfaces with changes unless they seem like clear strict improvements, especially if it doesn't feel like an extremely clear win, especially if it feels like it can be trivially layered on by the user.

But I do think factory is strictly better than default_factory in this context, and I think a backwards compatible alias is better than a breaking name change for a project like Pydantic.

@KeynesYouDigIt
Copy link

KeynesYouDigIt commented Nov 18, 2021

Let me know if I am in the wrong place, I came across this thread posing this question and have a question about v2

https://stackoverflow.com/questions/70026994/is-it-a-good-idea-to-create-derived-non-setable-fields-in-pydantic

Is there a way to allow a default_factory to access other values so the field can be derived from other values? Or is this the sort of thing that a process working with a pydantic class is supposed to handle (eg, we dont want to support derived data in the interest of keeping the class less complex)

What I have now works as intended (see the SO question), but I am wondering if there is or should be a more idiomatic/explicit way to do this?

In my mind it would look something like

class DynamicValueModel(BaseModel):
    field_1: str
    assigned_id: str

   @validator('assigned_id', derived=True)
   def hash(val, values):
       return hash(values['field_1'])
       
DynamicValueModel(assigned_id='oops_lol')
# raises ValidationException or ValueError
# 'Cannot assign value to derived field!
      

@polavishnu4444
Copy link

polavishnu4444 commented Oct 22, 2022

Is the default_factory expected to be triggered for None, empty("") values of string? If the field doesn't come in data then the default_facotry gets invoked, I was expecting to do the same in cases of None and empty string (explicitly) as the construct of Python variable foo or 'bar' would yield 'bar' in cases of foo being None or empty string variable.

@personage-hub
Copy link

personage-hub commented Nov 18, 2022

Is there any way to provide an argument to default_factory callable object?

class Foo(BaseModel):
    a: int
    b: int = (default_factory=bar(Foo.a))

@samuelcolvin
Copy link
Member

use partial.

@Bobronium
Copy link
Contributor

I was going to suggest partial as well, but I think, the real question from @personage-hub lies in his example: "Is there a way to provide model field as an argument to default_factory callable object?".

Quick answer: no.

You can achieve the effect you're looking for via @root_validator, though.

@vvanglro
Copy link

field-with-dynamic-default-value

Hi, I wonder if default_factory is still in beta in v1 latest version, or is it possible to use it in fastapi, Because I encountered a problem about this field in using fastapi.

relevant: default_factory of pydantic's Field not used when included in response_model

@krisrrr
Copy link

krisrrr commented Jun 21, 2024

is it correctly conceived that the value in default_factory generates once when importing the model? Then how is this different from just default=func()?

@Bobronium
Copy link
Contributor

Bobronium commented Jun 21, 2024

is it correctly conceived that the value in default_factory generates once when importing the model?

Not quite. default_factory is called to generate new value whenever model is instantiated.

You can quickly test this by using functions that return different value:

from datetime import datetime

from pydantic import BaseModel
from pydantic import Field

_counter = 0


def factory():
    global _counter
    try:
        return _counter
    finally:
        _counter += 1


class Model(BaseModel):
    counter: int = Field(default_factory=factory)
    timestamp: datetime = Field(default_factory=datetime.utcnow)


for i in range(3):
    print(Model())

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests