Skip to content

BaseModel.model_copy() seems to lose intersection data #3113

@oriori1703

Description

@oriori1703

Describe the Bug

I’m seeing what looks like a false positive bad-return when calling pydantic.BaseModel.model_copy() inside an isinstance branch on a generic type variable.

Notably, I included a control example with a similar copy(self) -> Self method (Copyable) that does type-check successfully in the same pattern. That suggests this is specific to pyrefly’s handling/integration of pydantic BaseModel.model_copy() rather than a general issue with Self + narrowed TypeVar.

Minimal repro

from copy import copy
from typing import Self, reveal_type

from pydantic import BaseModel


# Working example
class Copyable:
    def copy(self) -> Self:
        return copy(self)


class CopyableChild:
    pass


def test[T: Copyable](value: T) -> T:
    if isinstance(value, CopyableChild):
        reveal_type(value)  # revealed type is `CopyableChild & T`
        value_copy = value.copy()
        reveal_type(value_copy)  # revealed type is `CopyableChild & T`
        return value_copy
    else:
        reveal_type(value)
        return value


# Error reproduction
class ParenModel(BaseModel):
    field: int | str


class ChildModel(BaseModel):
    field: int


def test2[T: ParenModel](value: T) -> T:
    if isinstance(value, ChildModel):
        reveal_type(value)  # revealed type is `ChildModel & T`
        value_copy = value.model_copy()
        reveal_type(value_copy)  # revealed type is `ChildModel` instead of `ChildModel & T`
        return value_copy
    else:
        reveal_type(value)
        return value

pyrefly output:

> pyrefly check test.py
 INFO revealed type: CopyableChild & T [reveal-type]
  --> test.py:19:20
   |
19 |         reveal_type(value)  # revealed type is `CopyableChild & T`
   |                    -------
   |
 INFO revealed type: T [reveal-type]
  --> test.py:21:20
   |
21 |         reveal_type(value_copy)  # revealed type is `CopyableChild & T`
   |                    ------------
   |
 INFO revealed type: T [reveal-type]
  --> test.py:24:20
   |
24 |         reveal_type(value)
   |                    -------
   |
 INFO revealed type: ChildModel & T [reveal-type]
  --> test.py:39:20
   |
39 |         reveal_type(value)  # revealed type is `ChildModel & T`
   |                    -------
   |
 INFO revealed type: ChildModel [reveal-type]
  --> test.py:41:20
   |
41 |         reveal_type(value_copy)  # revealed type is `ChildModel` instead of `ChildModel & T`
   |                    ------------
   |
ERROR Returned type `ChildModel` is not assignable to declared return type `T` [bad-return]
  --> test.py:42:16
   |
37 | def test2[T: ParenModel](value: T) -> T:
   |                                       - declared return type
38 |     if isinstance(value, ChildModel):
39 |         reveal_type(value)  # revealed type is `ChildModel & T`
40 |         value_copy = value.model_copy()
41 |         reveal_type(value_copy)  # revealed type is `ChildModel` instead of `ChildModel & T`
42 |         return value_copy
   |                ^^^^^^^^^^
   |
 INFO revealed type: T [reveal-type]
  --> test.py:44:20
   |
44 |         reveal_type(value)
   |                    -------
   |
 INFO 1 error

Environment

  • pyrefly 0.60.2
  • Python 3.14.3
  • pydantic 2.12.5

Sandbox Link

No response

(Only applicable for extension issues) IDE Information

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions