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

Undocumented different behaviors of handling generic types in v1 and v2 #6994

Open
1 task done
haoyun opened this issue Aug 2, 2023 · 13 comments
Open
1 task done
Labels
bug V2 Bug related to Pydantic V2 documentation

Comments

@haoyun
Copy link

haoyun commented Aug 2, 2023

Initial Checks

  • I confirm that I'm using Pydantic V2

Description

I am creating a model A that has two type vars T and S.
I am trying to define a type as list[A[T, str], however with pydantic v2 I got error:

TypeError: There are no type variables left in list[__main__.A[~T, str]]

which does not happen with pydantic v1.

Example Code

from typing import Generic, TypeVar

from pydantic import BaseModel
from pydantic.v1 import BaseModel as BaseModelv1
from pydantic.v1.generics import GenericModel

T = TypeVar("T")
S = TypeVar("S")


# ==============================================
# Pydantic v1
# ==============================================
class A(GenericModel, Generic[T, S]):
    x: T
    y: S


B = list[A[T, str]]

C: B[int] = [A(x=1, y="a")]


class D(BaseModelv1):
    x: B[int]


# ==============================================
# Pydantic v2
# ==============================================


class AA(BaseModel, Generic[T, S]):
    x: T
    y: S


BB = list[AA[T, str]]


CC: BB[int] = [AA(x=1, y="a")]
# TypeError: There are no type variables left in list[__main__.AA[~T, str]]


class DD(BaseModel):
    x: BB[int]
    # TypeError: There are no type variables left in list[__main__.AA[~T, str]]


class EE(BaseModel):
    x: list[AA[int, str]] # without the alias BB, OK!

Python, Pydantic & OS Version

pydantic version: 2.0
        pydantic-core version: 2.0.1 release build profile
                 install path: /home/yun/.conda/envs/pydantic2/lib/python3.9/site-packages/pydantic
               python version: 3.9.17 (main, Jul  5 2023, 20:41:20)  [GCC 11.2.0]
                     platform: Linux-6.4.6-200.fc38.x86_64-x86_64-with-glibc2.37
     optional deps. installed: ['typing-extensions']

Selected Assignee: @lig

@haoyun haoyun added bug V2 Bug related to Pydantic V2 unconfirmed Bug not yet confirmed as valid/applicable labels Aug 2, 2023
@haoyun
Copy link
Author

haoyun commented Aug 2, 2023

I could replace the type alias B = list[A[T, str]] by a RootModel, but RootModel has problem with the type checker (pyright). For example in the following example, pyright warns that "list[AA[int, str]]" is incompatible with "BB[int]".

from typing import Generic, TypeVar

from pydantic import BaseModel, RootModel

T = TypeVar("T")
S = TypeVar("S")


class AA(BaseModel, Generic[T, S]):
    x: T
    y: S


BB = RootModel[list[AA[T, str]]]


class DD(BaseModel):
    x: BB[int]


# OK!
DD(x=BB[int]([AA[int, str](x=3, y="a")]))

# type checker error
# Pyright: Argument of type "list[AA[int, str]]" cannot be assigned to parameter "x" of type "BB[int]" in function "__init__"
#   "list[AA[int, str]]" is incompatible with "BB[int]" [reportGeneralTypeIssues]
DD(x=[AA[int, str](x=3, y="a")])


# Runtime Error and type checker error
# Input should be a valid dictionary or instance of AA[int, str] [type=model_type, input_value=AA(x=3, y='a'), input_type=AA]
DD(x=[AA(x=3, y="a")])

to get rid of the type checker error and runtime error, I must explicitly write all the concrete types.

@haoyun haoyun changed the title Inconsistency of handling generic types in v1 and v2 Undocumented different behaviors of handling generic types in v1 and v2 Aug 2, 2023
@NotTheEconomist
Copy link

I'm having the same issue. Did you find a workaround that didn't break your type checker?

Related SO question: https://stackoverflow.com/questions/76965613/there-are-no-type-variables-left-after-union-of-pydantic-models

@daniil-berg
Copy link

Here is a minimal reproducible example:

from pydantic import BaseModel
from typing import Generic, TypeVar

T = TypeVar("T")


class Model(BaseModel, Generic[T]):
    data: T


GenericAlias = Model[T] | None


def f() -> GenericAlias[int]: ...

Note: This causes different errors depending on the Python minor version used!

Error with Python 3.10.12:

Traceback (most recent call last):
  File "/path/to/module.py", line 14, in <module>
    def f() -> GenericAlias[int]: ...
TypeError: There are no type variables left in __main__.Model | None

Error with Python 3.11.4:

Traceback (most recent call last):
  File "/path/to/module.py", line 14, in <module>
    def f() -> GenericAlias[int]: ...
               ~~~~~~~~~~~~^^^^^
TypeError: __main__.Model | None is not a generic class

My system in both cases just with varying Python versions:

             pydantic version: 2.3.0
        pydantic-core version: 2.6.3
          pydantic-core build: profile=release pgo=true
                 install path: /path/to/pydantic
               python version: 3.10.12 (main, Aug 25 2023, 10:09:22) [GCC 13.2.1 20230801]
               /
               python version: 3.11.4 (main, Jun 24 2023, 20:38:45) [GCC 13.1.1 20230429]
                     platform: Linux-6.4.11-arch2-1-x86_64-with-glibc2.38
     optional deps. installed: ['devtools', 'email-validator', 'typing-extensions']

So far I was unable to figure out where that error is actually coming from, but suffice it to say the code is valid in terms of typing, passes Mypy and does not raise that error, if BaseModel is omitted from the bases of Model or replaced with some other class.

The fact that the stacktrace is so short could indicate that the error is emitted at the CPython level, but this is just conjecture. But if that is true, I would assume that Pydantic does something to the generic class that makes it appear non-generic at runtime.
Whether that happens in BaseModel.__class_getitem__, at the metaclass level or somewhere else, I don't know.

I found a possibly interesting, somewhat related discussion about implicitly generic type aliases.

@Alan3344
Copy link

Python 3.11.4 (main, Jul 25 2023, 17:36:13) [Clang 14.0.3 (clang-1403.0.22.14.1)] on darwin
TypeError: Fields of type "<class 'main.RespList'>" are not supported.

I am using FastAPI to write an api and need to return this general data structure. Unlike the above, I currently have no inheritance behavior, so it seems that generic types are not supported?

I use the generic type as the return type to display a more complete data structure in the OpenAPI UI

  • main.py file
from dataclasses import dataclass
from typing import Generic, TypeVar

from fastapi import FastAPI


app = FastAPI()
app.debug = True
T = TypeVar("T")


@dataclass
class UserData:
    id: int
    name: str
    age: int
    avatar: str


@dataclass
class BookData:
    id: int
    name: str
    use: int


@dataclass
class RespList(Generic[T]):
    code: int
    data: list[T]


@app.get("/api/getUser")
def index():
    return RespList[UserData](
        code=200,
        data=[
            UserData(
                id=i,
                name=f"test_{i}",
                age=i,
                avatar=f"https://avatars.dicebear.com/v2/avataaars/{i}.svg",
            ).__dict__
            for i in range(10)
        ],
    )


# run it> uvicorn main:app --reload

Below is the traceback
-- when i use RespList[UserData]

~ uvicorn main:app --reload
INFO:     Will watch for changes in these directories: ['/Users/alan/me/recatpy_']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [43575] using WatchFiles
Process SpawnProcess-1:
Traceback (most recent call last):
  File "/opt/homebrew/Cellar/python@3.11/3.11.4_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/process.py", line 314, in _bootstrap
    self.run()
  File "/opt/homebrew/Cellar/python@3.11/3.11.4_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "/opt/homebrew/lib/python3.11/site-packages/uvicorn/_subprocess.py", line 76, in subprocess_started
    target(sockets=sockets)
  File "/opt/homebrew/lib/python3.11/site-packages/uvicorn/server.py", line 61, in run
    return asyncio.run(self.serve(sockets=sockets))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.4_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/runners.py", line 190, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.4_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "uvloop/loop.pyx", line 1517, in uvloop.loop.Loop.run_until_complete
  File "/opt/homebrew/lib/python3.11/site-packages/uvicorn/server.py", line 68, in serve
    config.load()
  File "/opt/homebrew/lib/python3.11/site-packages/uvicorn/config.py", line 467, in load
    self.loaded_app = import_from_string(self.app)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/uvicorn/importer.py", line 21, in import_from_string
    module = importlib.import_module(module_str)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.4_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1204, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1176, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1147, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 690, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 940, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/Users/alan/me/recatpy_/main.py", line 33, in <module>
    @app.get("/api/getUser")
     ^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/fastapi/routing.py", line 704, in decorator
    self.add_api_route(
  File "/opt/homebrew/lib/python3.11/site-packages/fastapi/routing.py", line 643, in add_api_route
    route = route_class(
            ^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/fastapi/routing.py", line 448, in __init__
    self.response_field = create_response_field(
                          ^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/fastapi/utils.py", line 99, in create_response_field
    return ModelField(**kwargs)  # type: ignore[arg-type]
           ^^^^^^^^^^^^^^^^^^^^
  File "pydantic/fields.py", line 436, in pydantic.fields.ModelField.__init__
  File "pydantic/fields.py", line 552, in pydantic.fields.ModelField.prepare
  File "pydantic/fields.py", line 755, in pydantic.fields.ModelField._type_analysis
TypeError: Fields of type "<class 'main.RespList'>" are not supported.

dict[str, int | list[dict[str, int | str]]] or dict[str, int | list[UserData]]

@app.get("/api/getUser")
def index() -> dict[str, int | list[dict[str, int | str]]]:
    return dict(
        code=200,
        data=[
            dict(
                id=i,
                name=f"test_{i}",
                age=i,
                avatar=f"https://avatars.dicebear.com/v2/avataaars/{i}.svg",
            )
            for i in range(10)
        ],
    )

image
image

I can redeclare the class structure, but it's too much trouble.

@Alan3344
Copy link

Python 3.11.4 (main, Jul 25 2023, 17:36:13) [Clang 14.0.3 (clang-1403.0.22.14.1)] on darwin TypeError: Fields of type "<class 'main.RespList'>" are not supported.

I am using FastAPI to write an api and need to return this general data structure. Unlike the above, I currently have no inheritance behavior, so it seems that generic types are not supported?

I use the generic type as the return type to display a more complete data structure in the OpenAPI UI

  • main.py file
from dataclasses import dataclass
from typing import Generic, TypeVar

from fastapi import FastAPI


app = FastAPI()
app.debug = True
T = TypeVar("T")


@dataclass
class UserData:
    id: int
    name: str
    age: int
    avatar: str


@dataclass
class BookData:
    id: int
    name: str
    use: int


@dataclass
class RespList(Generic[T]):
    code: int
    data: list[T]


@app.get("/api/getUser")
def index():
    return RespList[UserData](
        code=200,
        data=[
            UserData(
                id=i,
                name=f"test_{i}",
                age=i,
                avatar=f"https://avatars.dicebear.com/v2/avataaars/{i}.svg",
            ).__dict__
            for i in range(10)
        ],
    )


# run it> uvicorn main:app --reload

Below is the traceback -- when i use RespList[UserData]

~ uvicorn main:app --reload
INFO:     Will watch for changes in these directories: ['/Users/alan/me/recatpy_']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [43575] using WatchFiles
Process SpawnProcess-1:
Traceback (most recent call last):
  File "/opt/homebrew/Cellar/python@3.11/3.11.4_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/process.py", line 314, in _bootstrap
    self.run()
  File "/opt/homebrew/Cellar/python@3.11/3.11.4_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "/opt/homebrew/lib/python3.11/site-packages/uvicorn/_subprocess.py", line 76, in subprocess_started
    target(sockets=sockets)
  File "/opt/homebrew/lib/python3.11/site-packages/uvicorn/server.py", line 61, in run
    return asyncio.run(self.serve(sockets=sockets))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.4_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/runners.py", line 190, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.4_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "uvloop/loop.pyx", line 1517, in uvloop.loop.Loop.run_until_complete
  File "/opt/homebrew/lib/python3.11/site-packages/uvicorn/server.py", line 68, in serve
    config.load()
  File "/opt/homebrew/lib/python3.11/site-packages/uvicorn/config.py", line 467, in load
    self.loaded_app = import_from_string(self.app)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/uvicorn/importer.py", line 21, in import_from_string
    module = importlib.import_module(module_str)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.4_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1204, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1176, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1147, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 690, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 940, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/Users/alan/me/recatpy_/main.py", line 33, in <module>
    @app.get("/api/getUser")
     ^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/fastapi/routing.py", line 704, in decorator
    self.add_api_route(
  File "/opt/homebrew/lib/python3.11/site-packages/fastapi/routing.py", line 643, in add_api_route
    route = route_class(
            ^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/fastapi/routing.py", line 448, in __init__
    self.response_field = create_response_field(
                          ^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/fastapi/utils.py", line 99, in create_response_field
    return ModelField(**kwargs)  # type: ignore[arg-type]
           ^^^^^^^^^^^^^^^^^^^^
  File "pydantic/fields.py", line 436, in pydantic.fields.ModelField.__init__
  File "pydantic/fields.py", line 552, in pydantic.fields.ModelField.prepare
  File "pydantic/fields.py", line 755, in pydantic.fields.ModelField._type_analysis
TypeError: Fields of type "<class 'main.RespList'>" are not supported.

dict[str, int | list[dict[str, int | str]]] or dict[str, int | list[UserData]]

@app.get("/api/getUser")
def index() -> dict[str, int | list[dict[str, int | str]]]:
    return dict(
        code=200,
        data=[
            dict(
                id=i,
                name=f"test_{i}",
                age=i,
                avatar=f"https://avatars.dicebear.com/v2/avataaars/{i}.svg",
            )
            for i in range(10)
        ],
    )

image image

I can redeclare the class structure, but it's too much trouble.

Name: pydantic
Version: 1.10.12
Summary: Data validation and settings management using python type hints
Home-page: https://github.com/pydantic/pydantic
Author: Samuel Colvin
Author-email: s@muelcolvin.com
License: MIT
Location: /opt/homebrew/lib/python3.11/site-packages
Requires: typing-extensions
Required-by: copier, fastapi, flet

When I write this question to end...
After reading everyone's comments,I found the reason by trying, and finally used the latest version pydantic 2.3.0
itself has been resolved 👍
image

@lig
Copy link
Contributor

lig commented Aug 28, 2023

@haoyun Could you please check the behavior again?

It looks like #7006 is related to this and it was fixed lately.

@NotTheEconomist
Copy link

NotTheEconomist commented Aug 29, 2023

@haoyun Could you please check the behavior again?

It looks like #7006 is related to this and it was fixed lately.

I'm not haoyun, but I can repro on 2.3.0 with his original example. @daniil-berg has a more minimal example that also errors on 2.3.0.

~/tmp/so is  v0.1.0
❯ python -V
Python 3.10.12

~/tmp/so is  v0.1.0
❯ python -c 'import pydantic.version; print(pydantic.version.version_info())'
             pydantic version: 2.3.0
        pydantic-core version: 2.6.3
          pydantic-core build: profile=release pgo=true
                 install path: /home/asmith/.cache/pypoetry/virtualenvs/so-u1ptCtqB-py3.10/lib/python3.10/site-packages/pydantic
               python version: 3.10.12 (main, Jun  7 2023, 12:45:35) [GCC 9.4.0]
                     platform: Linux-5.15.90.1-microsoft-standard-WSL2-x86_64-with-glibc2.31
     optional deps. installed: ['typing-extensions']

~/tmp/so is  v0.1.0
❯ python main.py
Traceback (most recent call last):
  File "/home/asmith/tmp/so/main.py", line 21, in <module>
    C: B[int] = [A(x=1, y="a")]
TypeError: There are no type variables left in list[__main__.A[T, str]]

Indeed I get the same error even if I install from the main branch or even the tip commit of the PR 5338def. Here's main:

❯ pip freeze | grep pydantic
pydantic @ git+https://github.com/pydantic/pydantic.git@dad6665e986325c61494f3431160df7e4e8d90f0
pydantic_core==2.6.3

~/tmp/so is  v0.1.0
❯ python main.py
Traceback (most recent call last):
  File "/home/asmith/tmp/so/main.py", line 21, in <module>
    C: B[int] = [A(x=1, y="a")]
TypeError: There are no type variables left in list[__main__.A[T, str]]

@pipeworks-asmith
Copy link

A relevant observation: listing the generic as the first parent of the class removes the buggy behavior.

T = TypeVar("T")


# exhibits the buggy behavior
class BuggyModel(BaseModel, Generic[T]):
    data: T

# does not!
class FixedModel(Generic[T], BaseModel):
    data: T

@dmontagu
Copy link
Contributor

dmontagu commented Aug 31, 2023

The problem here is that, for various reasons, the value returned by BaseModel.__class_getitem__ is not an instance of typing._GenericAlias, but a class of its own. Note that you may run into other bugs if you try putting Generic[T] as the first class there, since you'll get a _GenericAlias as the result of the class getitem, rather than a model subclass as intended.

The problem is that when the typing module tries to parametrize the generic parametrized list, it runs into problems because it doesn't recognize that that parameter is still a parameter of the inner model.

I believe this is fixable through some reworking of the BaseModel.__class_getitem__ function and associated generics functionality, but I think it may be a lot of work. PR definitely welcome if someone wants to try but it gets a bit hairy.

I know it may be unsatisfying, but my suggestion would be to make an alias for A[T, str] instead of list[A[T, str]] and use that, so instead of using:

B = list[A[T, str]]

I would use

B = A[T, str]
# Instead of B[int], you now do list[B[int]]

Hopefully we can get this fixed properly one day so your snippet "just works", but I think it will take some time.

@dmontagu dmontagu removed the unconfirmed Bug not yet confirmed as valid/applicable label Aug 31, 2023
@lig
Copy link
Contributor

lig commented Aug 31, 2023

Also, I think detecting this behavior in BaseModel.__class_getitem__ and giving a warning could be a good first step.

@samuelcolvin
Copy link
Member

we need to document the behaviour, and add a warning when the parents are used in the "wrong" order.

@NotTheEconomist
Copy link

NotTheEconomist commented Sep 4, 2023

we need to document the behaviour, and add a warning when the parents are used in the "wrong" order.

Hmm yes, but currently there's no right order, yeah?

class GenericFirst(Generic[T], BaseModel):
    """Produces a _GenericAlias object, not a submodel"""
    pass

class ModelFirst(BaseModel, Generic[T]):
    """Produce a submodel, but exposes the bug described in this ticket"""
    pass

# namely:

T1 = ModelFirst | str | int
T2 = GenericFirst | str | int
T1[list]  # fails, but shouldn't!
T2[list]  # succeeds the way the previous line should!

@alexmojaki
Copy link
Contributor

add a warning when the parents are used in the "wrong" order.

I opened #7845 (before even seeing this) and started working on it. A few tests specifically related to parents in the wrong order started failing, and digging into the history led me here.

The problem here is that, for various reasons, the value returned by BaseModel.__class_getitem__ is not an instance of typing._GenericAlias, but a class of its own. Note that you may run into other bugs if you try putting Generic[T] as the first class there, since you'll get a _GenericAlias as the result of the class getitem, rather than a model subclass as intended.

The problem is that when the typing module tries to parametrize the generic parametrized list, it runs into problems because it doesn't recognize that that parameter is still a parameter of the inner model.

Yes, specifically there are places where it could pick up __parameters__ from the model class but it doesn't because it expects to get those from an object like a _GenericAlias, not a class:

https://github.com/python/cpython/blob/37e4e20eaa8f27ada926d49e5971fecf0477ad26/Lib/typing.py#L262-L264

https://github.com/python/cpython/blob/37e4e20eaa8f27ada926d49e5971fecf0477ad26/Lib/typing.py#L1293-L1295

One even has a comment:

We don't want __parameters__ descriptor of a bare Python class.

I was able to work around this somewhat with some monkeypatching:

import typing
from typing import Generic, TypeVar, List, Union

from pydantic import BaseModel


def patch_method(original):
    def patched(self, args, *rest):
        if not isinstance(args, tuple):
            args = (args,)
        args = tuple(
            typing._GenericAlias(t, t.__parameters__)
            if isinstance(t, type)
            and issubclass(t, BaseModel)
            and hasattr(t, "__parameters__")
            else t
            for t in args
        )
        return original(self, args, *rest)

    return patched


typing._GenericAlias.__getitem__ = typing._tp_cache(
    patch_method(typing._GenericAlias.__getitem__)
)
typing._GenericAlias._make_substitution = patch_method(
    typing._GenericAlias._make_substitution
)
typing._SpecialGenericAlias.__getitem__ = typing._tp_cache(
    patch_method(typing._SpecialGenericAlias.__getitem__)
)


T = TypeVar("T")


class A(BaseModel, Generic[T]):
    pass


L = List[A[T]]
print(L[int])
# typing.List[__main__.A[int]]

U = Union[list[T], A[T]]
print(U[str])
# typing.Union[list[str], __main__.A[str]]

U2 = List[T] | A[T]
print(U2[float])
# typing.Union[typing.List[float], __main__.A[float]]

Note that this code isn't exhaustive and it may not even be correct, but I think it demonstrates the point. However it doesn't work when the typing module isn't involved, i.e. when using only builtin types like list instead of List and the | operator instead of typing.Union, so e.g. list[A[T]] or A[T] | None don't work. This is for the same kind of reason as before: some internal code sees a class object and ignores its __parameters__. But now the internal code is written in C:

https://github.com/python/cpython/blob/main/Objects/genericaliasobject.c#L252

Hopefully we can get this fixed properly one day so your snippet "just works", but I think it will take some time.

It seems that a full solution supporting | and builtin generic types (i.e. supporting the most convenient syntax) would require either:

  1. Monkeypatching C code,
  2. Upstream changes in Python, or
  3. BaseModel.__class_getitem__ returning something that isn't a type, at least when some of the arguments are type variables.

In the meantime, this comment seems fair:

Hmm yes, but currently there's no right order, yeah?

I'd like to know if inheriting from Generic before BaseModel is still considered to always be wrong, in which case I'll go ahead with adding a warning. Or can this issue be considered as an argument in favour of sometimes doing it that way? Will the problems caused by the wrong order always be dealbreakers?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug V2 Bug related to Pydantic V2 documentation
Projects
None yet
Development

No branches or pull requests

9 participants