Skip to content

Conversation

Viicos
Copy link
Member

@Viicos Viicos commented Jun 17, 2025

Change Summary

Fixes (partially) #11613.

The remaining failing tests require:

both to be available in 3.14a4.

This PR adds basic support for Python 3.14. More 3.14 specific features (e.g. python/cpython#114051) will be added in follow up PRs.

Remaining issues

Using Field() in dataclasses

To support the following:

from pydantic import Field

from pydantic.dataclasses import dataclass  # Also with stdlib dataclasses

@dataclass
class A:
    a: int = Field(default=1)

We currently have a somewhat hacky solution, which involves wrapping Field(default=1) into dataclasses.field(default=Field(default=1)), and more importantly directly writing into the __annotations__ dict (L184):

def make_pydantic_fields_compatible(cls: type[Any]) -> None:
"""Make sure that stdlib `dataclasses` understands `Field` kwargs like `kw_only`
To do that, we simply change
`x: int = pydantic.Field(..., kw_only=True)`
into
`x: int = dataclasses.field(default=pydantic.Field(..., kw_only=True), kw_only=True)`
"""
for annotation_cls in cls.__mro__:
annotations: dict[str, Any] = getattr(annotation_cls, '__annotations__', {})
for field_name in annotations:
field_value = getattr(cls, field_name, None)
# Process only if this is an instance of `FieldInfo`.
if not isinstance(field_value, FieldInfo):
continue
# Initialize arguments for the standard `dataclasses.field`.
field_args: dict = {'default': field_value}
# Handle `kw_only` for Python 3.10+
if sys.version_info >= (3, 10) and field_value.kw_only:
field_args['kw_only'] = True
# Set `repr` attribute if it's explicitly specified to be not `True`.
if field_value.repr is not True:
field_args['repr'] = field_value.repr
setattr(cls, field_name, dataclasses.field(**field_args))
# In Python 3.9, when subclassing, information is pulled from cls.__dict__['__annotations__']
# for annotations, so we must make sure it's initialized before we add to it.
if cls.__dict__.get('__annotations__') is None:
cls.__annotations__ = {}
cls.__annotations__[field_name] = annotations[field_name]

In 3.14, writing/accessing __annotations__ is no longer safe, as a NameError can be raised if a an unresolvable annotation is used. In other words, this now raises:

from pydantic import Field

from pydantic.dataclasses import dataclass

@dataclass
class A:  # raises at declaration
    a: Forward = Field(default=1)

Forward = int

Unfortunately, I've tried hard to see if this could be supported somehow, and came to the conclusion that there is no way to do so without an unreasonable amount of workarounds. Such usage of forward references (without using stringified annotations) is only going to get more popular as 3.14 adoption grows, but it is hard to know how common this issue will be encountered. A couple notes:

  • This can be mitigated if we only want to support using Field() on the class being decorated (in which case we don't need to write to __annotations__) but not on any super-classes.

  • To support the case where Field() is defined on a super-class, this super-class needs to be a Pydantic dataclass (in which case the previous point would have handled it). If the super-class is a stdlib dataclass, it will not be possible to reasonably support this without too much hassle:

    @stdlib_dataclass
    class A:
         a: int = pydantic.Field(kw_only=True)
    
    @pydantic.dataclasses.dataclass
    class B(A):
        b: int

Here are the options we have:

  • Deprecate support for repr and kw_only support together with Field() for dataclasses. I'm not a fan of this approach, as the issue only arises when using forward references as a deferred annotation, and works fine in other cases.
  • Catch any NameError exceptions when trying to write to __annotations__, and raise a user warning saying the usage of Field() won't be supported if we catch one. Not a fan of this either, as the end user will only get a warning without any real explanation as to why it happened (they don't have to be aware of the Pydantic internals implemented to support Field()).

I don't have any satisfactory answers for now. I have added an xfailing test for this, and we can revisit later.

Edit: tracked in #12045, a different implementation will be used.

Real deferred annotations

PEP 649/749 should now allow use cases like this:

def outer():
    def inner():
        class Model(BaseModel):
            ann: Annotated[List[Dict[str, str]], MaxLen(1)]

        Dict = dict

        return Model

    List = list

    Model = inner()

    return Model

Model = outer()

Model.__annotations__['ann']
#> Annotated[list[dict[str, str]], MaxLen(1)]

However, trying to rebuild Model after getting it from outer() will raise an exception. This is because when we rebuild model fields, we do so individually by using typing._eval_type(). In reality, we should try accessing __annotations__ again to let CPython internals resolve the references for us. This is far from trivial to implement, so deferred for a follow-up PR.

@github-actions github-actions bot added the relnotes-fix Used for bugfixes. label Jun 17, 2025
Copy link

cloudflare-workers-and-pages bot commented Jun 17, 2025

Deploying pydantic-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 26c0811
Status: ✅  Deploy successful!
Preview URL: https://938ce782.pydantic-docs.pages.dev
Branch Preview URL: https://3-14-initial-support.pydantic-docs.pages.dev

View logs

Copy link
Contributor

github-actions bot commented Jun 17, 2025

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  pydantic
  dataclasses.py
  pydantic/_internal
  _config.py
  _fields.py
  _generics.py
  _model_construction.py
  _typing_extra.py
Project Total  

This report was generated by python-coverage-comment-action

Copy link

codspeed-hq bot commented Jun 17, 2025

CodSpeed Performance Report

Merging #11991 will not alter performance

Comparing 3.14-initial-support (26c0811) with main (dac3c43)

Summary

✅ 46 untouched benchmarks

@Viicos Viicos added relnotes-feature needs-blogpost-entry This PR needs to be documented in the release notes blog post and removed relnotes-fix Used for bugfixes. labels Jul 2, 2025
@Viicos Viicos force-pushed the 3.14-initial-support branch from c85a95f to 6df3dfd Compare July 5, 2025 13:42
@Viicos Viicos force-pushed the 3.14-initial-support branch from 6df3dfd to b4e8b0a Compare July 6, 2025 10:11
@Viicos Viicos force-pushed the 3.14-initial-support branch from 8ec2fbe to 06befc9 Compare July 6, 2025 17:25
@Viicos Viicos added the third-party-tests Add this label on a PR to trigger 3rd party tests label Jul 7, 2025
@Viicos Viicos closed this Jul 7, 2025
@Viicos Viicos reopened this Jul 7, 2025
@Viicos Viicos force-pushed the 3.14-initial-support branch 3 times, most recently from beeba50 to bef4d6a Compare July 10, 2025 07:04
@Viicos Viicos force-pushed the 3.14-initial-support branch from bef4d6a to 3acae68 Compare July 10, 2025 07:10
@Viicos Viicos marked this pull request as ready for review July 10, 2025 07:13
Copy link
Contributor

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

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

LGTM with some final nits!

Comment on lines +144 to +146
- name: Install memray system dependencies
if: ${{ matrix.python-version == '3.14' && matrix.os == 'ubuntu-latest' }}
run: sudo apt-get install libunwind-dev libdebuginfod-dev
Copy link
Contributor

Choose a reason for hiding this comment

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

This is to build memray from source?

If so, it might be that for free-threading we can also build memray: bloomberg/memray#772

Copy link
Member Author

Choose a reason for hiding this comment

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

@Viicos Viicos force-pushed the 3.14-initial-support branch from b7fbcdb to 26c0811 Compare July 10, 2025 09:23
@Viicos Viicos merged commit 9452e13 into main Jul 10, 2025
95 checks passed
@Viicos Viicos deleted the 3.14-initial-support branch July 10, 2025 09:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-blogpost-entry This PR needs to be documented in the release notes blog post relnotes-feature third-party-tests Add this label on a PR to trigger 3rd party tests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants