Skip to content
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

Use positional-only self in BaseModel constructor, so no field name can ever conflict with it. #8072

Merged
merged 1 commit into from Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/concepts/validators.md
Expand Up @@ -726,10 +726,10 @@ def init_context(value: Dict[str, Any]) -> Iterator[None]:
class Model(BaseModel):
my_number: int

def __init__(__pydantic_self__, **data: Any) -> None:
__pydantic_self__.__pydantic_validator__.validate_python(
def __init__(self, /, **data: Any) -> None:
self.__pydantic_validator__.validate_python(
data,
self_instance=__pydantic_self__,
self_instance=self,
context=_init_context_var.get(),
)

Expand Down
2 changes: 1 addition & 1 deletion pydantic/_internal/_generate_schema.py
Expand Up @@ -2187,7 +2187,7 @@ def generate_pydantic_signature(
# Make sure the parameter for extra kwargs
# does not have the same name as a field
default_model_signature = [
('__pydantic_self__', Parameter.POSITIONAL_OR_KEYWORD),
('self', Parameter.POSITIONAL_ONLY),
('data', Parameter.VAR_KEYWORD),
]
if [(p.name, p.kind) for p in present_params] == default_model_signature:
Expand Down
7 changes: 3 additions & 4 deletions pydantic/main.py
Expand Up @@ -150,18 +150,17 @@ class BaseModel(metaclass=_model_construction.ModelMetaclass):
__pydantic_complete__ = False
__pydantic_root_model__ = False

def __init__(__pydantic_self__, **data: Any) -> None: # type: ignore
def __init__(self, /, **data: Any) -> None: # type: ignore
"""Create a new model by parsing and validating input data from keyword arguments.

Raises [`ValidationError`][pydantic_core.ValidationError] if the input data cannot be
validated to form a valid model.

`__init__` uses `__pydantic_self__` instead of the more common `self` for the first arg to
allow `self` as a field name.
`self` is explicitly positional-only to allow `self` as a field name.
"""
# `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks
__tracebackhide__ = True
__pydantic_self__.__pydantic_validator__.validate_python(data, self_instance=__pydantic_self__)
self.__pydantic_validator__.validate_python(data, self_instance=self)

# The following line sets a flag that we use to determine when `__init__` gets overridden by the user
__init__.__pydantic_base_init__ = True
Expand Down
10 changes: 10 additions & 0 deletions pydantic/mypy.py
Expand Up @@ -1148,6 +1148,16 @@ def add_method(
first = [Argument(Var('_cls'), self_type, None, ARG_POS, True)]
else:
self_type = self_type or fill_typevars(info)
# `self` is positional *ONLY* here, but this can't be expressed
# fully in the mypy internal API. ARG_POS is the closest we can get.
# Using ARG_POS will, however, give mypy errors if a `self` field
# is present on a model:
#
# Name "self" already defined (possibly by an import) [no-redef]
#
# As a workaround, we give this argument a name that will
# never conflict. By its positional nature, this name will not
# be used or exposed to users.
Comment on lines +1151 to +1160
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for adding this explanation 👍

first = [Argument(Var('__pydantic_self__'), self_type, None, ARG_POS)]
args = first + args

Expand Down
4 changes: 2 additions & 2 deletions pydantic/root_model.py
Expand Up @@ -52,15 +52,15 @@ def __init_subclass__(cls, **kwargs):
)
super().__init_subclass__(**kwargs)

def __init__(__pydantic_self__, root: RootModelRootType = PydanticUndefined, **data) -> None: # type: ignore
def __init__(self, /, root: RootModelRootType = PydanticUndefined, **data) -> None: # type: ignore
__tracebackhide__ = True
if data:
if root is not PydanticUndefined:
raise ValueError(
'"RootModel.__init__" accepts either a single positional argument or arbitrary keyword arguments'
)
root = data # type: ignore
__pydantic_self__.__pydantic_validator__.validate_python(root, self_instance=__pydantic_self__)
self.__pydantic_validator__.validate_python(root, self_instance=self)

__init__.__pydantic_base_init__ = True

Expand Down
8 changes: 8 additions & 0 deletions tests/test_edge_cases.py
Expand Up @@ -1296,6 +1296,14 @@ class Model(BaseModel):
}


def test_no_name_conflict_in_constructor():
class Model(BaseModel):
self: int

m = Model(**{'__pydantic_self__': 4, 'self': 2})
assert m.self == 2


def test_self_recursive():
class SubModel(BaseModel):
self: int
Expand Down