Docstrings for type aliases

Hi! I propose preserving a string literal immediately following a type statement as the resulting alias object’s __doc__ attribute, with support in pydoc to display it in help(), and making that docstring retrievable from ast.TypeAlias.

type Timeout = float | None
"""
Maximum wait in seconds.

Use None to wait indefinitely, or zero to return immediately.
"""

Under this proposal, Timeout.__doc__ would contain that documentation, and help(Timeout) would be:

Help on type alias Timeout in module mymodule:

type Timeout = float | None
    Maximum wait in seconds.

    Use None to wait indefinitely, or zero to return immediately.

    Lazy value access:

    __value__
        Lazily evaluated value of the type alias.

    evaluate_value
        Evaluation function for __value__.

    See help(typing.TypeAliasType) for the full type alias interface.

Motivation

The type statement creates a dedicated runtime object, so it can carry its own documentation, as classes and functions do.

Keeping the docstring on the type alias would let help() and other inspection tools read it at runtime without finding or parsing the source file. The documentation would also stay with the alias when it is re-exported or passed to another component.

Currently, help() on type aliases displays generic information about TypeAliasType, as shown in #156925.

Several tools already support documentation for type aliases exactly in the proposed format. Pyright supports docstrings following type statements (since 2023), Sphinx’s autotype documents PEP 695 aliases and their docstrings (since 2025), and Pylint recognises these strings as documentation (since 2023).

Runtime documentation could also be consumed by third-party frameworks. For example, frameworks that already recognize TypeAliasType (such as Pydantic) could choose to use __doc__ as descriptive metadata. Such integrations would be up to those projects.

Prototype

I have implementations of compiler and runtime support and pydoc support that formats the interface of type aliases and distinguishes type aliases from other module members.

Aliases created through the TypeAliasType constructor can be documented by assigning to __doc__, as there is no new constructor parameter.

Technicalities

Saving / stripping the docstring

AST preprocessing records the following string in a new optional doc field on ast.TypeAlias.
Under -OO, preprocessing strips it instead, using shared handling for astfold_body and nested statement sequences (including a little bit of refactoring to improve readability). Additional docstrings (as specified in PEP 257) are also handled, i.e. they cannot replace the stripped docstring.

Reading the docstring from the AST

The field contains the original text and is available through ast.get_docstring().

For example, ast.get_docstring(tree.body[0]) returns "Maximum wait in seconds." for this tree:

>>> tree = ast.parse('type Timeout = float | None\n"Maximum wait in seconds."')
>>> print(ast.dump(tree, indent=4))
Module(
    body=[
        TypeAlias(
            name=Name(id='Timeout', ctx=Store()),
            value=BinOp(
                left=Name(id='float'),
                op=BitOr(),
                right=Constant(value=None)),
            doc='Maximum wait in seconds.'),
        Expr(
            value=Constant(value='Maximum wait in seconds.'))])

Both ast.dump() and repr() display the string in the doc field and in the original Expr(Constant(...)) statement. Keeping the statement preserves the structure used by existing AST consumers, so tools can still find documentation in its original position. For an undocumented alias, node.doc is None and the default dump omits that field. ast.get_docstring(node) cleans indentation and surrounding blank lines; ast.get_docstring(node, clean=False) returns the original string stored in node.doc.

Open question: AST representation

The prototype stores the docstring in TypeAlias.doc while retaining the following string statement. If a tool changes one without updating the other, a populated doc field takes precedence during compilation, while ast.unparse() uses the statement. This is the current implementation’s behavior; I’d welcome feedback on what the specified behavior should be.

Another option is for doc to reference the original Constant node. That would share in-place edits, but visitors would reach the node through both references, and replacing one reference could still leave them pointing to different nodes. The representation and how AST transformations should handle it remain open for discussion.

Or, we could hide the doc attribute completely behind a private attribute only retrievable through ast.get_docstring().

help() integration

Like pydoc’s existing function-signature rendering, pydoc requests a string representation through annotationlib.Format.STRING to display help(). String-format evaluation can execute code with side effects (such as reifying lazy imports). If rendering raises an exception, it tries to extract the original expression from the source without evaluating it. If the source cannot be recovered, it displays a placeholder showing a representation of the exception and continues with the documentation.

The implementations include tests for docstring handling and text and HTML rendering.


What do you think of adding this support? I’d be interested in your use cases, concerns, and thoughts about any parts of the proposal.

23 Likes

I like this a lot as it makes type aliases more first class citizens.

Two questions:

Of course, those projects can choose whatever they want. However, wouldn’t Annotated be more appropriate for metadata? (Zen “one way” etc.)

I assume this would also work for doctest; correct?

1 Like

What’s there to test though :thinking:

1 Like

I’d really like this feature for constants as well

PI = 3.14
"The circle constant."

MISSING = sentinel("MISSING")
"A sentinel to represent a missing value."

but there’s unfortunately no obvious container to hang the .__doc__ on.

2 Likes

That fundamentally can’t be done if it’s a standard object like an int or a float, but you may want to consider an enum. They get convenient reprs automatically, and you could add additional information if needed.

You cant do that with enum without implementing a custom __new__ right?

Which I think brings up the larger topic that the community wants…

It seems like the community really wants a way to have first class support in documentation through help() for constants within a namespace, whether that be a module attribute, say the PI example or a class variable.

1 Like

Three cases:

  • example code snippets are at least checked on syntax and spelling, although indeed type = doesn’t provide much to test
    An example of use could be given as doctest
    type Timeout = float | None
    """
    Maximum wait in seconds.
    
    Use None to wait indefinitely, or zero to return immediately, e.g.
    >>> def wait(t: Timeout): ...
    """
    
  • you could however test basic typing at runtime
    Given
    def is_typealias_instance(obj: object, alias: TypeAliasType, /) -> bool:
        return isinstance(obj, alias.__value__)
    
    a doctest could go
    type Timeout = float | None
    """
    Maximum wait in seconds.
    
    Use None to wait indefinitely, or zero to return immediately.
    
    >>> assert is_typealias_instance(1.0, Timeout)
    """
    
  • if doctest gets similar functionality as pytest-mypy · PyPI gives to pytest · PyPI, we can test advanced typing at runtime
1 Like

It depends on what you mean by that.

You can have PI with name and docstring using only standard tools, although it’s a little verbose (2 extra lines compared to @tmk’s example:

@enum.global_enum
class _(float, enum.ReprEnum):
    """The circle constant."""
    PI = 3.14

print(repr(PI), PI, PI.__doc__)
# __main__.PI 3.14 The circle constant.

However, help(PI) floods your console with help on float and Enum. If you only want help info on the constant, then that’s not possible this way.

1 Like

My idea with Pydantic was analogous to model class docstrings that already populate description automatically.

>>> from pydantic import BaseModel, TypeAdapter
...
... class Foo(BaseModel):
...     """An object"""
...
... print(TypeAdapter(Foo).json_schema())
...
{'description': 'An object', 'properties': {}, 'title': 'Foo', 'type': 'object'}

Not a part of this proposal for now, but I guess it’s okay to support. Adding doctest discovery for type aliases doesn’t seem to pose much of maintenance costs, but I will appreciate more inputs from others on this.

I’d also find documentation for constants useful. The reason I’ve limited this proposal to type statements is that they already create a separate object that can own a docstring. I’d prefer to discuss documentation of constants separately, so this proposal can be evaluated on its own.

5 Likes

Thanks for clarifying. For description it makes sense, because that’s the JSON’s equivalent to __doc__.
“Metadata” made me imagine a minilanguage in docstrings (intentionally made look ugly in this example) along the lines of

class Foo(BaseModel):
    """An object.

    ![PROPERTIES]
    !!whatever = 1
"""
print(TypeAdapter(Foo).json_schema())
# {'description': 'An object', 'properties': {'whatever': 1}, 'title': 'Foo', 'type': 'object'}

Of course, pydantic could do that, but I’m glad your idea doesn’t suggest it.

1 Like

My prototype makes the attribute writable, this is very much in scope of the proposal.

3 Likes

This seems like a pretty easy win with no obvious downsides I’m noticing.

1 Like

I don’t have the time currently to consider if there’s a better way to handle the AST than this (though what’s proposed for it seems fine to me, it would be worth checking with any major tools that use ast parsing)

The idea itself is solid, and something that’s come up a few times in some prior discussions; As a result of some of why it came up before (eg typing.Doc) I think there needs to be a clear indication that the documentation of the type alias should be seen as “attached” to all uses of the type alias, and that in cases where a type should be interpreted in some semantically different way despite being the same type system type, a seperate alias should be used.

That’s more an intersection of typing and documentation tools issue, but getting ahead of it with a prescriptive intent for such docstrings will avoid tooling making conflicting decisions on handling these.

2 Likes

I like the idea. Arbitrary constant docstrings would be nice, but we don’t need to block on them to get some incremental improvements.

I’m not a huge fan, although I understand that all of the options have trade-offs.

Is it too breaking of a change to suggest that the following string statement is not retained in the AST? Not having a separate “duplicate” node really seems like the best way to avoid these problems, if I’ve understood correctly.

It would be potentially breaking for a narrow class of users, but it can only possibly impact 3.12+ code.

1 Like

For what it’s worth, static analysis tools were perfectly able to fetch the docstring in the node following an assignment until now, so technically they do not need the docstring to be attached to the type alias node.

1 Like

Were there specific reasons you initially went with storing the docstring in a doc field on the type alias node, in addition to keeping the string node? Is it just for convenience? Consistency with other nodes? A requirement for being able to compile the string into the type’s __doc__ attribute?

1 Like

Added AST node field to be able to read it back ast.get_docstring(), and preserved the string node to not break people.

1 Like

Would ast.get_docstring() be able to pick up the existing node after the assignment :thinking:? I suppose not if the type node doesn’t reference the next node, or their parent.

OK so it’s both for consistency and compatibility. Thanks!

Just a note that as maintainer of Griffe, I’d be fine with not having ast.get_docstring() work for type nodes. I’m already visiting the AST so I can find the docstring myself :slightly_smiling_face:

2 Likes

Would ast.get_docstring() be able to pick up the existing node after the assignment :thinking:? I suppose not if the type node doesn’t reference the next node, or their parent.

Yeah, and having to supply the parent would still make the function have to rewalk all nodes to find the TA+1 node which is suboptimal as it usually happens in a loop already walking nodes. Some call sites (that I’ve seen on GitHub) don’t have the access to anything except the targeted node.

2 Likes

I’m a little confused about the issues around the AST. If the grammar is something like

type_alias:
    | "type" NAME [type_params] '=' expression [NEWLINE string+]

that doesn’t create any ambiguity, because of PEG’s checking rules in order, and it would allow adding the docstring to the AST node cleanly as an extra attribute.

Or not?

1 Like