-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Add ability to use Final in a field type annotation #2768
Conversation
@samuelcolvin @PrettyWood Could you please review this PR? I want to hear your opinion regarding this feature. |
IMHO, this implementation adds an unexpected behavior from the pydanticish perspective (a default value turns what would otherwise be an instance field into a class var) without a clear benefit. from pydantic import BaseModel
class Model(BaseModel):
var: Final[str] = Field(max_length=3) I presume that in a case like this no one would expect to have an immutable class var of the type The aim of a final name is not to be reassigned after initialization, but there's no such initialization in a Model declaration. As with from pydantic import BaseModel
class A:
var = 1
class B(BaseModel):
var = 1 >>> A.var
1
>>> B.var
...
AttributeError: type object 'B' has no attribute 'var' I think The behavior of from dataclasses import dataclass
from typing import Final, TypedDict
@dataclass
class A:
var: Final[str]
#> error: Final name must be initialized with a value
class B(TypedDict):
var: Final[str]
#> error: Final can be only used as an outermost qualifier in a variable annotation
# this also raises a TypeError
#> TypeError: typing.Final[str] is not valid as type argument But mypy's |
Honestly I'm against it. I feel like it's really confusing and the different behaviour with or without a default value goes against pydantic one (required vs optional more or less). Edit: I like the "synonym for immutable" (to me this is what seems the most reasonable) hence setting |
@nuno-andre @PrettyWood Thanks for your comments. @nuno-andre Thanks for pointing out case when the field declared as a Regarding the mypy. Currently, pydantic mypy plugin handles a lot of cases and for instance, it generates @PrettyWood Currently class Model(BaseModel):
a: int # required
b: int = 10 # optional
c: int = Field() # required
d: int = Field(default=10) # optional
e: int = Field(default_factory=lambda: 10) # optional To simplify things we can always use class Model(BaseModel):
a: Final[int] = 10 # instance field with default value 10 same as Field(default=10) Another feature of this PR is that we can have init-only fields. class Model(BaseModel):
a: Final[int]
b: int
obj = Model(a=10, b=20)
obj.a = 100 # will fail because final field can't be changed outside of init method
obj.b = 100 # totally okay As far as implementation follows |
@uriyyo My point was just about class var / field. By setting a default value, we change a field in a class var, which is what the PEP says but feels anti-pydantic to me. |
@PrettyWood Thanks for your opinion, it's really important for me🙂 I totally agree with you that we can use |
I think we should get more feedback on this before adding anything or reviewing. |
# Conflicts: # tests/test_main.py
@PrettyWood @samuelcolvin Any chance to review this one? It would be great to hear your opinion. I would like to use this feature in my projects) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
otherwise LGTM.
pydantic/main.py
Outdated
@@ -183,12 +184,17 @@ def __new__(mcs, name, bases, namespace, **kwargs): # noqa C901 | |||
def is_untouched(v: Any) -> bool: | |||
return isinstance(v, untouched_types) or v.__class__.__name__ == 'cython_function_or_method' | |||
|
|||
def is_finalvar_with_default_val(type_: Type[Any], val: Any) -> bool: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be moved into the module scope of fields.py
.
@@ -347,6 +353,10 @@ def __setattr__(self, name, value): # noqa: C901 (ignore complexity) | |||
raise ValueError(f'"{self.__class__.__name__}" object has no field "{name}"') | |||
elif not self.__config__.allow_mutation or self.__config__.frozen: | |||
raise TypeError(f'"{self.__class__.__name__}" is immutable and does not support item assignment') | |||
elif name in self.__fields__ and self.__fields__[name].final: | |||
raise TypeError( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -386,10 +387,21 @@ def _check_classvar(v: Optional[Type[Any]]) -> bool: | |||
return v.__class__ == ClassVar.__class__ and getattr(v, '_name', None) == 'ClassVar' | |||
|
|||
|
|||
def _check_finalvar(v: Optional[Type[Any]]) -> bool: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you add a docstring explaining what this is doing.
assert 'a' not in Model.__class_vars__ | ||
assert 'a' in Model.__fields__ | ||
|
||
assert Model.__fields__['a'].final |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we we add a check somewhere else that final
is normally false.
please update. |
you'll also need to rebase/merge master to get tests passing. |
please review |
thanks so much. |
A PR has just been opened to change the Python typing specifications that will propagate to any use of dataclass-likes in the Python ecosystem, such as with The above typing PR specifies behaviour which is incompatible with the pydantic/pydantic/_internal/_fields.py Lines 169 to 171 in 8128821
The typing PR will mean that As |
Add ability to use
Final
in a field type annotationImplementation is based on a
PEP 591
:So in this case field
name
will be treated as aClassVar
:But in a case when a default value is not present than field will be treated as an init-only required field:
Related issue number
#2766
Checklist
changes/<pull request or issue id>-<github username>.md
file added describing change(see changes/README.md for details)