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

Pydantic fails with Python 3.10 new UnionType #3300

Closed
3 tasks done
DrJackilD opened this issue Oct 7, 2021 · 24 comments
Closed
3 tasks done

Pydantic fails with Python 3.10 new UnionType #3300

DrJackilD opened this issue Oct 7, 2021 · 24 comments
Labels
bug V1 Bug related to Pydantic V1.X

Comments

@DrJackilD
Copy link

DrJackilD commented Oct 7, 2021

Checks

  • I added a descriptive title to this issue
  • I have searched (google, github) for similar issues and couldn't find anything
  • I have read and followed the docs and still think this is a bug

Bug

Pydantic fails with a new-style unios, presented in Python 3.10. The reason is straightforward - it simply doesn't know about UnionType.

The problem is here: pydantic.fields.py:529 where we check the origin for type Union.

Since the new UnionType is compatible with typing.Union (according to documentation) it's probably should be easy to fix. I could do this, but I need to understand, what could be affected by this. Is it will be enough to check all usages of Union type and fix it carefully? Also, it's probably should be some kind of a function is_union to make it work for versions before 3.10.

Additional info

Output of python -c "import pydantic.utils; print(pydantic.utils.version_info())":

pydantic version: 1.8.2
            pydantic compiled: False
                 install path: /Users/drjackild/.pyenv/versions/3.10.0/envs/groups/lib/python3.10/site-packages/pydantic
               python version: 3.10.0 (default, Oct  7 2021, 17:08:09) [Clang 13.0.0 (clang-1300.0.29.3)]
                     platform: macOS-11.6-x86_64-i386-64bit
     optional deps. installed: ['typing-extensions']
from pydantic import BaseModel

class A(BaseModel):
    s: str | None

Raises exception:

    
Traceback (most recent call last):
  File "/Users/drjackild/.pyenv/versions/groups/lib/python3.10/site-packages/IPython/core/interactiveshell.py", line 3444, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-16-a6ee5ea43ee8>", line 1, in <module>
    class A(BaseModel):
  File "/Users/drjackild/.pyenv/versions/groups/lib/python3.10/site-packages/pydantic/main.py", line 299, in __new__
    fields[ann_name] = ModelField.infer(
  File "/Users/drjackild/.pyenv/versions/groups/lib/python3.10/site-packages/pydantic/fields.py", line 411, in infer
    return cls(
  File "/Users/drjackild/.pyenv/versions/groups/lib/python3.10/site-packages/pydantic/fields.py", line 342, in __init__
    self.prepare()
  File "/Users/drjackild/.pyenv/versions/groups/lib/python3.10/site-packages/pydantic/fields.py", line 451, in prepare
    self._type_analysis()
  File "/Users/drjackild/.pyenv/versions/groups/lib/python3.10/site-packages/pydantic/fields.py", line 626, in _type_analysis
    raise TypeError(f'Fields of type "{origin}" are not supported.')
TypeError: Fields of type "<class 'types.UnionType'>" are not supported.
@DrJackilD DrJackilD added the bug V1 Bug related to Pydantic V1.X label Oct 7, 2021
@DrJackilD
Copy link
Author

Nevermind, already found in master, that you did exactly what I thought to do 😄

I will wait for the new release, thank you for your work!

@DrJackilD DrJackilD changed the title Pydantic fails with Python 3.10 new style union (UnionType Pydantic fails with Python 3.10 new UnionType Oct 7, 2021
@samuelcolvin
Copy link
Member

I'll try to get a new release out asap.

luckydonald added a commit to luckydonald/fastorm that referenced this issue Oct 13, 2021
@k0t3n
Copy link

k0t3n commented Oct 18, 2021

@samuelcolvin any ETA?

@rmzr7
Copy link

rmzr7 commented Oct 19, 2021

+1 on this

@DrJackilD
Copy link
Author

DrJackilD commented Oct 20, 2021

I'll reopen the issue just to keep it visible until it gets released

@DrJackilD DrJackilD reopened this Oct 20, 2021
@Bobronium
Copy link
Contributor

Bobronium commented Oct 23, 2021

Here's a workaround until it gets released.

from types import UnionType
from typing import Union

from pydantic.main import ModelMetaclass, BaseModel


class MyModelMetaclass(ModelMetaclass):
    def __new__(cls, name, bases, namespace, **kwargs):
        annotations = namespace.get("__annotations__", {})
        for annotation_name, annotation_type in annotations.items():
            if isinstance(annotation_type, UnionType):
                annotations[annotation_name] = Union.__getitem__(annotation_type.__args__)
        return super().__new__(cls, name, bases, namespace, **kwargs)


class MyBaseModel(BaseModel, metaclass=MyModelMetaclass):
    ...


class Python310(MyBaseModel):
    a: str | None


print(repr(Python310(a="3.10")))

Same approach code also works for SQLModel, but instead of inheriting from BaseModel and MyModelMetaclass, you'll need to inherit from SQLModel and SQLModelMetaclass

Update: rather fragile addition that also supports ForwardRef's
from types import UnionType
from typing import Union

from pydantic.main import ModelMetaclass, BaseModel


class MyModelMetaclass(ModelMetaclass):
    def __new__(cls, name, bases, namespace, **kwargs):
        annotations = namespace.get("__annotations__", {})
        for annotation_name, annotation_type in annotations.items():
            if isinstance(annotation_type, UnionType):
                annotations[annotation_name] = Union.__getitem__(
                    annotation_type.__args__
                )
            elif isinstance(annotation_type, str) and "|" in annotation_type:
                # it's rather naive to think that having | can only mean that it's a Union
                # It will work in most cases, but you need to be aware that it's not always the case
                annotations[
                    annotation_name
                ] = f'Union[{", ".join(map(str.strip, annotation_type.split("|")))}]'

        return super().__new__(cls, name, bases, namespace, **kwargs)


class MyBaseModel(BaseModel, metaclass=MyModelMetaclass):
    ...


class Python310(MyBaseModel):
    a: str | None
    b: "WhatIsThat | int | None"


class WhatIsThat(BaseModel):
    foo = "1"


Python310.update_forward_refs()

print(repr(Python310(a="3.10", b=None)))

@roganartu
Copy link

I think there's a related bug that, based on the existing fix in master, I think would also be fixed by this. I wanted to share the trace here in case it helps someone googling, but didn't think it was worthy of a separate issue. If you disagree let me know and I'll raise one.

Given:

from typing import Optional

import pydantic

class Settings(pydantic.BaseSettings):
    works: Optional[str] = None
    broken: str | None = None

Settings()

One gets:

/tmp/pydantic via 🐍 v3.10.0 (env) on ☁️  (us-east-1) took 2s 
❯ ./poc.py          

/tmp/pydantic via 🐍 v3.10.0 (env) on ☁️  (us-east-1) took 142ms 
❯ works=test ./poc.py

/tmp/pydantic via 🐍 v3.10.0 (env) on ☁️  (us-east-1) took 148ms 
❯ works=test broken=test ./poc.py
Traceback (most recent call last):
  File "/tmp/pydantic/env/lib/python3.10/site-packages/pydantic/env_settings.py", line 172, in __call__
    env_val = settings.__config__.json_loads(env_val)  # type: ignore
  File "/home/tl/.pyenv/versions/3.10.0/lib/python3.10/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
  File "/home/tl/.pyenv/versions/3.10.0/lib/python3.10/json/decoder.py", line 337, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "/home/tl/.pyenv/versions/3.10.0/lib/python3.10/json/decoder.py", line 355, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/tmp/pydantic/./poc.py", line 10, in <module>
    Settings()
  File "/tmp/pydantic/env/lib/python3.10/site-packages/pydantic/env_settings.py", line 37, in __init__
    **__pydantic_self__._build_values(
  File "/tmp/pydantic/env/lib/python3.10/site-packages/pydantic/env_settings.py", line 63, in _build_values
    return deep_update(*reversed([source(self) for source in sources]))
  File "/tmp/pydantic/env/lib/python3.10/site-packages/pydantic/env_settings.py", line 63, in <listcomp>
    return deep_update(*reversed([source(self) for source in sources]))
  File "/tmp/pydantic/env/lib/python3.10/site-packages/pydantic/env_settings.py", line 174, in __call__
    raise SettingsError(f'error parsing JSON for "{env_name}"') from e
pydantic.env_settings.SettingsError: error parsing JSON for "broken"

/tmp/pydantic via 🐍 v3.10.0 (env) on ☁️  (us-east-1) took 137ms 
✗❯ works=test broken='"test"' ./poc.py
Traceback (most recent call last):
  File "/tmp/pydantic/./poc.py", line 10, in <module>
    Settings()
  File "/tmp/pydantic/env/lib/python3.10/site-packages/pydantic/env_settings.py", line 36, in __init__
    super().__init__(
  File "/tmp/pydantic/env/lib/python3.10/site-packages/pydantic/main.py", line 406, in __init__
    raise validation_error
pydantic.error_wrappers.ValidationError: 1 validation error for Settings
broken
  instance of UnionType expected (type=type_error.arbitrary_type; expected_arbitrary_type=UnionType)

The JSON error confused me when googling this initially after updating a FastAPI project of mine to 3.10 and making no other changes. I believe it happens because the lack of support for UnionType breaks the field.is_complex() check, causing pydantic to attempt to parse it as JSON.

@AlexanderPodorov
Copy link

+1 on this

@Toreno96
Copy link

Toreno96 commented Dec 4, 2021

+1

@Bobronium
Copy link
Contributor

Bobronium commented Dec 4, 2021

If I may, I'd like to discourage people from posting comments like "+1".

The issue is already confirmed and acknowledged by maintainer, fix will be available with the next release: #3300 (comment).

These comments are noisy for everyone subscribed to this issue, while providing no value at all.

You can subscribe to this issue if you wish to be notified when it will be fixed.

In the meanwhile you can use this as a workaround: #3300 (comment).

@Toreno96
Copy link

Toreno96 commented Dec 4, 2021

@Bobronium sorry for the noise then.

For me personally, "+1" comments are having the value of noticing the maintainer there are actually people interested in the fix (and I see this practice is very common in open-source projects, so I wasn't aware this can be bad), especially considering the #3300 (comment) was actually from a few months ago and there's still no release yet.

But you're arguments are perfectly valid and if you see the alternative solution for that (do you see if and how many people subscribe to the issue?) then I'm happy to oblige 😅

@Bobronium
Copy link
Contributor

@Toreno96, no worries. Sorry if my comment read too grumpy 🙆🏻.

Let me elaborate on why I personally think that "+1" comments are usually a bad idea.

Let's start with better alternative first: reactions.
I think it's a great feature for showing the number of people agree/interested/grateful/upset with a comment/issue.
Unlike comments, it doesn't increase the amount of noise and saves the issue from being flooded: imagine googling this issue and seeing 30+ comments with plain "+1" or "same here". It's not hard to imagine that people won't even attempt to read the rest of the comments, and instead will post yet another "+1" and leave.

While I understand your concern and desire to give a notice to the maintainer, I don't think "+1" would be the best approach for that. I believe maintainers are already very aware of this, and many other issues. I think some form of discussion here would be more applicable if, for example, pydantic had a new release that didn't address this issue. But currently, It's not a question "whether this issue will be fixed", or even "when" (fix is already in master), but "when will see the new release".

So I'm not discouraging any conversations, I just believe that "+1" is not a great start for one, especially in given situation :)

@artemijan
Copy link

artemijan commented Dec 17, 2021

Hi guys, I believe the fix suggested in #3300 (comment) will not be working for the following case:

class SericesList(BaseModel):
    services: list[EcsServiceBase|EcsServiceState]

Toreno96 added a commit to apptension/onetimepass that referenced this issue Jan 4, 2022
You can see in the changes, that I alternate between the `typing.Optional` and new union operator `|`, which introduces inconsistency.

This is because:
1. It seems the union operator `|` will be the preferred way to annotate the optionals: <https://twitter.com/raymondh/status/1472426960349548549?s=20>

    I think it makes sense, and tbh I like this syntax, so I decided to incorporate this preference into the project.
1. Unfortunately, Pydantic does not support the new union operator `|` at all yet: <pydantic/pydantic#3300>.

    I've tried the [suggested workaround](pydantic/pydantic#3300 (comment)), but it didn't work.
    
    As such, I decided to use `typing.Optional` in the case of Pydantic models, as an exception to the consistency.

---

* Forbid extra fields in the Pydantic model

* Store `label` and `issuer` parameters
@Halkcyon
Copy link

Halkcyon commented Jan 25, 2022

I remember running into this problem when testing 3.10 with Pydantic and found this topic. It appears like it's no longer a problem with the release of 1.9 (#2885), so the problem is resolved.

@Toreno96
Copy link

Toreno96 commented Feb 2, 2022

Sorry for reopening the issue, and sorry if that's not the best place to ask this question, but IMHO it's pretty related:

Python 3.7 also supports the new UnionType if from __future__ import annotations is used:

$ python
Python 3.7.12 (default, Oct 13 2021, 06:51:32)
[Clang 11.0.0 (clang-1100.0.33.17)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> foo: str | None = None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'
>>> from __future__ import annotations
>>> foo: str | None = None
>>> foo is None
True

But it does not work in Pydantic 1.9:
example.py

from __future__ import annotations

from pydantic import BaseModel


class Foo(BaseModel):
    bar: str | None
$ python example.py
Traceback (most recent call last):
  File "a.py", line 6, in <module>
    class Foo(BaseModel):
  File "pydantic/main.py", line 187, in pydantic.main.ModelMetaclass.__new__
  File "pydantic/typing.py", line 356, in pydantic.typing.resolve_annotations
    if self._name == 'Optional':
  File "/usr/local/Cellar/python@3.7/3.7.12_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/typing.py", line 263, in _eval_type
    return t._evaluate(globalns, localns)
  File "/usr/local/Cellar/python@3.7/3.7.12_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/typing.py", line 467, in _evaluate
    eval(self.__forward_code__, globalns, localns),
  File "<string>", line 1, in <module>
TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'

Is it something that could be supported in the future, or such backward compatibility is out of scope?

@ikamensh
Copy link

ikamensh commented Feb 9, 2022

same as above happens for python 3.8

@PrettyWood
Copy link
Member

PrettyWood commented Feb 9, 2022

Hi everyone
You need to understand that int | str is not valid at runtime for python < 3.10 as type doesn't implement __or__.
It is valid at interpretation time by adding from __future__ import annotations but pydantic can't do magic and change how python works at runtime.
I opened a while ago #2609, which works for python < 3.10 by using https://github.com/PrettyWood/future-typing, which changes at interpretation time int | str into Union[int, str] for example. But I don't think it's worth merging as moving to python 3.10 is safer
Hope it clarifies things

@Toreno96
Copy link

@PrettyWood thank you very much for the answer and detailed explanation 🙏

br3ndonland added a commit to br3ndonland/inboard that referenced this issue Apr 2, 2022
This commit will update type annotation syntax for Python 3.10. The
project currently also supports Python 3.8 and 3.9, so the annotations
are imported with `from __future__ import annotations`.

The Python 3.10 union operator (the pipe, like `str | None`) will not be
used on pydantic models. If running Python 3.9 or below, pydantic is not
compatible with the union operator, even if annotations are imported
with `from __future__ import annotations`.

https://peps.python.org/pep-0604/
https://docs.python.org/3/whatsnew/3.10.html
pydantic/pydantic#2597 (comment)
pydantic/pydantic#2609 (comment)
pydantic/pydantic#3300 (comment)
@GregHilston
Copy link

GregHilston commented Sep 3, 2022

I'm actually experiencing this on Python 3.10.4 with Pydantic 1.10.1.

class PromptData(BaseModel):
    image = PIL.Image.Image | None

results in:

    class PromptData(BaseModel):
  File "pydantic/main.py", line 222, in pydantic.main.ModelMetaclass.__new__
  File "pydantic/fields.py", line 506, in pydantic.fields.ModelField.infer
  File "pydantic/fields.py", line 436, in pydantic.fields.ModelField.__init__
  File "pydantic/fields.py", line 557, in pydantic.fields.ModelField.prepare
  File "pydantic/fields.py", line 831, in pydantic.fields.ModelField.populate_validators
  File "pydantic/validators.py", line 752, in find_validators
RuntimeError: no validator found for <class 'types.UnionType'>, see `arbitrary_types_allowed` in Config

Adding the following to PromptData

class Config:
    arbitrary_types_allowed = True  # Required so our Union in the type of instance variable image can be checked

results in

  File "pydantic/main.py", line 242, in pydantic.main.ModelMetaclass.__new__
  File "pydantic/class_validators.py", line 181, in pydantic.class_validators.ValidatorGroup.check_for_unused
pydantic.errors.ConfigError: Validators defined with incorrect fields: prevent_none (use check_fields=False if you're inheriting from the model and intended this)

@Bobronium
Copy link
Contributor

Bobronium commented Sep 3, 2022

@GregHilston, seems like a different issue to me. Nevermind. See below for minimal reproduction example.

Old post

First of all, does this work as expected?

class PromptData(BaseModel):
    image = PIL.Image.Image

    class Config:
        arbitrary_types_allowed = True 

What about this?

from typing import Optional

class PromptData(BaseModel):
    image = Optional[PIL.Image.Image]

    class Config:
        arbitrary_types_allowed = True 

I also would appreciate if you'd post complete examples, including required imports and pip packages.

Upd.

Actually, I'm able to reproduce this as well:

             pydantic version: 1.10.1
            pydantic compiled: True
                 install path: .venv/lib/python3.10/site-packages/pydantic
               python version: 3.10.0 (default, Oct 21 2021, 22:41:19) [Clang 12.0.5 (clang-1205.0.22.11)]
                     platform: macOS-12.3.1-arm64-arm-64bit
     optional deps. installed: ['typing-extensions']
from pydantic import BaseModel

class M(BaseModel):
    a = int | None
Traceback (most recent call last):
  File "repro.py", line 4, in <module>
    class M(BaseModel):
  File "pydantic/main.py", line 222, in pydantic.main.ModelMetaclass.__new__
  File "pydantic/fields.py", line 506, in pydantic.fields.ModelField.infer
  File "pydantic/fields.py", line 436, in pydantic.fields.ModelField.__init__
  File "pydantic/fields.py", line 557, in pydantic.fields.ModelField.prepare
  File "pydantic/fields.py", line 831, in pydantic.fields.ModelField.populate_validators
  File "pydantic/validators.py", line 752, in find_validators
RuntimeError: no validator found for <class 'types.UnionType'>, see `arbitrary_types_allowed` in Config

Ok, this is strange:

from typing import Optional
from pydantic import BaseModel

class M(BaseModel):
    a = Optional[str]

gives me

RuntimeError: no validator found for <class 'typing._UnionGenericAlias'>, see `arbitrary_types_allowed` in Config

Makes me feel like my setup is broken (which can be the case since I'm also playing with pdm).

Full reproduction log:

~/temp/test
$ python -m venv venv

~/temp/test
$ . venv/bin/activate

~/temp/test (venv)
$ pip install pydantic
Collecting pydantic
  Downloading pydantic-1.10.1-cp310-cp310-macosx_11_0_arm64.whl (2.6 MB)
     |████████████████████████████████| 2.6 MB 1.6 MB/s
Collecting typing-extensions>=4.1.0
  Using cached typing_extensions-4.3.0-py3-none-any.whl (25 kB)
Installing collected packages: typing-extensions, pydantic
Successfully installed pydantic-1.10.1 typing-extensions-4.3.0
WARNING: You are using pip version 21.2.3; however, version 22.2.2 is available.
You should consider upgrading via the '/Users/rocky/temp/test/venv/bin/python -m pip install --upgrade pip' command.

~/temp/test (venv)
$ pbpaste > repro.py

~/temp/test (venv)
$ python repro.py
Traceback (most recent call last):
  File "/Users/rocky/temp/test/repro.py", line 4, in <module>
    class M(BaseModel):
  File "pydantic/main.py", line 222, in pydantic.main.ModelMetaclass.__new__
  File "pydantic/fields.py", line 506, in pydantic.fields.ModelField.infer
  File "pydantic/fields.py", line 436, in pydantic.fields.ModelField.__init__
  File "pydantic/fields.py", line 557, in pydantic.fields.ModelField.prepare
  File "pydantic/fields.py", line 831, in pydantic.fields.ModelField.populate_validators
  File "pydantic/validators.py", line 752, in find_validators
RuntimeError: no validator found for <class 'typing._UnionGenericAlias'>, see `arbitrary_types_allowed` in Config

~/temp/test (venv)
$ python -c "from pydantic import version; print(version.version_info())"
             pydantic version: 1.10.1
            pydantic compiled: True
                 install path: /Users/rocky/temp/test/venv/lib/python3.10/site-packages/pydantic
               python version: 3.10.0 (default, Oct 21 2021, 22:41:19) [Clang 12.0.5 (clang-1205.0.22.11)]
                     platform: macOS-12.3.1-arm64-arm-64bit
     optional deps. installed: ['typing-extensions']

@Bobronium Bobronium mentioned this issue Sep 3, 2022
6 tasks
@GregHilston
Copy link

@Bobronium My apologies on the weak minimum code example. Thanks for doing this leg work to better understand what I was saying :)

@hramezani
Copy link
Member

shouldn't we replace = with : when we define variables in model?
a = Optional[str] -> a: Optional[str]

@Bobronium
Copy link
Contributor

Ah, of course... Thank you so much! I thought I started to go crazy.

@GregHilston
Copy link

@hramezani Oh my god. That's hilarious that I totally didn't see that... I'm embarrassed ha

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

No branches or pull requests