Skip to content

Calling help() on a dataclass shows dataclass internal names instead of type names in 3.14 or later #137530

Closed

Description

@DavidCEllis

Bug report

Bug description:

If you call help on a dataclass in Python 3.14 or later it shows the internal type names used for the exec call in the signature.

Docstring included to hide the dataclasses generated __doc__ which would just confuse things.

from dataclasses import dataclass

@dataclass
class Example:
    "docstring"
    x: int

help(Example)

Before, on 3.13

Help on class Example in module __main__:

class Example(builtins.object)
 |  Example(x: int) -> None
 |
 |  docstring
 |
 |  Methods defined here:
 |
 |  __eq__(self, other)
 |      Return self==value.
 |
 |  __init__(self, x: int) -> None
 |      Initialize self.  See help(type(self)) for accurate signature.
...

Now, on 3.14 or later

Help on class Example in module __main__:

class Example(builtins.object)
 |  Example(x: __dataclass_type_x__) -> __dataclass___init___return_type__
 |
 |  docstring
 |
 |  Methods defined here:
 |
 |  __eq__(self, other)
 |      Return self==value.
 |
 |  __init__(self, x: __dataclass_type_x__) -> __dataclass___init___return_type__
 |      Initialize self.  See help(type(self)) for accurate signature.
...

This looks a bit messy and reveals inner details that users of help probably don't need to see.

It looks like this comes from inspect.signature(cls, annotation_format=Format.STRING) being used in pydoc, but I think it's a dataclasses issue.

attrs doesn't suffer the same fate as it attaches annotations as __annotations__ instead of defining them in the source code string to be evaluated.

Inspired by that, for my own dataclass-like implementation for 3.14+ I'm collecting the annotations and generating an __annotate__ function which appears to solve this issue: see here

dataclasses could potentially do something similar.

CPython versions tested on:

3.14, CPython main branch

Operating systems tested on:

No response

Linked PRs

Activity

  1. ZeroIntensity commented on Aug 8, 2025

    @ZeroIntensity
    Member

    This is a 3.14 regression, so I've marked it as a blocker for now. This doesn't look like something that can easily be fixed, but we should come to a decision before 3.14.0.

  2. sobolevn commented on Aug 8, 2025

    @sobolevn
    Member

    Idea: we can at least define a table with translation. Something like:

    class Example(builtins.object)
     |  Example(x: __dataclass_type_x__) -> __dataclass___init___return_type__
     |
     |  docstring
     |
     |  Internal magic names:
     |     __dataclass___init___return_type__ -> None
     |     __dataclass_type_x__ -> int
     |
     |  Methods defined here:
     |
     |  __eq__(self, other)
     |      Return self==value.
     |
     |  __init__(self, x: __dataclass_type_x__) -> __dataclass___init___return_type__
     |      Initialize self.  See help(type(self)) for accurate signature.

    What do you think?

    CC @JelleZijlstra

  3. JelleZijlstra commented on Aug 8, 2025

    @JelleZijlstra
    Member

    That's unfortunate, thanks for noticing!

    I think we should fix this but it doesn't seem critical for 3.14.0 since it's not runtime behavior; it can wait for 3.14.1.

    I don't think @sobolevn's translation table is a great solution; ideally the internal names shouldn't bleed to users at all. Maybe we can attach a custom __annotate__ function that outputs nicer STRING format outputs?

  4. self-assigned this
    on Aug 8, 2025
  5. sobolevn commented on Aug 8, 2025

    @sobolevn
    Member

    Ok, I will try this out: custom __annotate__ seems like an interesting idea.

  6. DavidCEllis commented on Aug 8, 2025

    @DavidCEllis
    ContributorAuthor

    The issue is caused by get_annotations with Format.STRING attempting to get the text from the source code and in this case succeeding, but the output is not what you actually want users to see.

    These values are specifically from the generated __init__ function.

    It may be worth noting that if you have a forward reference in your dataclass annotations, this will also appear in __annotations__ (and get_annotations with format.VALUE).

    from dataclasses import dataclass
    from annotationlib import get_annotations, Format
    
    @dataclass
    class Example:
        x: undefined
    
    print(get_annotations(Example.__init__, format=Format.STRING))
    # {'x': '__dataclass_type_x__', 'return': '__dataclass___init___return_type__'}
    
    print(Example.__init__.__annotations__)
    # {'x': ForwardRef('undefined', is_class=True, owner=<class '__main__.Example'>), 'return': None}

    Maybe we can attach a custom annotate function that outputs nicer STRING format outputs?

    As linked earlier, this is what I currently have for my own implementation in ducktools-classbuilder

    from annotationlib import Format, ForwardRef, type_repr
    
    def make_annotate_function(annos):
        # Construct an annotation function from __annotations__
        def annotate_func(format):
            match format:
                case Format.VALUE | Format.FORWARDREF:
                    return {
                        k: v.evaluate(format=format)
                        if isinstance(v, ForwardRef) else v
                        for k, v in annos.items()
                    }
                case Format.STRING:
                    string_annos = {}
                    for k, v in annos.items():
                        if isinstance(v, str):
                            string_annos[k] = v
                        elif isinstance(v, ForwardRef):
                            string_annos[k] = v.evaluate(format=Format.STRING)
                        else:
                            string_annos[k] = type_repr(v)
                    return string_annos
                case _:
                    raise NotImplementedError(format)
        return annotate_func

    Where annos would be {f.name: f.type for f in fields} with the resulting function attached to the __init__ method.

    Edit: Along with "return": None included in the annotation dict
    Edit 2: And possibly with some extra filtering for f.init.

  7. added
    3.14bugs and security fixes
    3.15bugs and security fixes
    on Aug 8, 2025
  8. hugovk commented on Aug 12, 2025

    @hugovk
    Member

    I think we should fix this but it doesn't seem critical for 3.14.0 since it's not runtime behavior; it can wait for 3.14.1.

    Agreed, let's wait for 3.14.1 and move to deferred-blocker.

  9. hugovk commented on Oct 14, 2025

    @hugovk
    Member

    3.14.0 is out, it's time to consider this for 3.14.1.

  10. DavidCEllis commented on Oct 14, 2025

    @DavidCEllis
    ContributorAuthor

    I've kind of been sitting on this PR: #137711

    Other than probably needing some clean up/review, the problem I have with the state of that PR is VALUE annotations remain somewhat broken. An init=False field with a forward reference will break them even though that field isn't necessary for the __init__ annotations.

    Unless dataclasses is going to start messing with annotationlib internals I think you either have to live with that or allow ForwardRef values to potentially leak into the VALUE annotations by using those as the source instead.

    There is the possibility of just generating string annotations and attaching them to __annotations__ as if this were generated under __future__ annotations. Those would at least be consistent even if it would also defeat the point of the new deferred annotations.

    I proposed this idea on discuss which would provide the tools needed to solve this cleanly (and for third party dataclass-like implementations) but there seemed to be absolutely no interest. Even so I imagine something like that might only be usable in a future release so we may need either a compromise or some internal work for 3.14.

  11. JelleZijlstra commented on Oct 20, 2025

    @JelleZijlstra
    Member

    An init=False field with a forward reference will break them

    That doesn't seem like the worst thing to me; it's generally true in 3.14 that VALUE format annotations are sometimes not available, and you have to use FORWARDREF/STRING in those cases. Not ideal that VALUE here doesn't work when in theory it could work, but I think your PR is a great improvement already.

  12. DavidCEllis commented on Oct 21, 2025

    @DavidCEllis
    ContributorAuthor

    It's an improvement for STRING and FORWARDREF annotations. Technically VALUE can now fail in places it would have "succeeded" before (by leaking forward references).

    I don't like that it has to replicate the logic of the field gathering in order to do this. This isn't too bad for dataclasses, but other implementations will have extra things (such as attrs converters) which need to be taken into account when generating annotations that are going to make their annotate functions more complicated.

  13. added a commit that references this issue on Nov 10, 2025
  14. added 2 commits that reference this issue on Nov 10, 2025
  15. encukou commented on Nov 11, 2025

    @encukou
    Member

    Thank you @DavidCEllis for the report and fix, and @JelleZijlstra for reviews!

  16. added a commit that references this issue on Dec 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

3.14bugs and security fixes3.15bugs and security fixesdeferred-blockerstdlibStandard Library Python modules in the Lib/ directorytopic-dataclassestype-bugAn unexpected behavior, bug, or error

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions