Skip to content

Enable py3.14 and deprecate some alias functionality#225

Open
nstarman wants to merge 6 commits intobeartype:masterfrom
nstarman:mnt/py314
Open

Enable py3.14 and deprecate some alias functionality#225
nstarman wants to merge 6 commits intobeartype:masterfrom
nstarman:mnt/py314

Conversation

@nstarman
Copy link
Copy Markdown
Collaborator

@nstarman nstarman commented Nov 23, 2025

Aliases are now always turned on. The alias machinery dynamically creates TypeAliasType objects that are ONLY used in string representations, never in the actual type.

Fixes #224
Fixes #227

@coveralls
Copy link
Copy Markdown

coveralls commented Nov 23, 2025

Pull Request Test Coverage Report for Build 23405591937

Details

  • 114 of 115 (99.13%) changed or added relevant lines in 4 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage decreased (-0.09%) to 99.506%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/plum/_alias.py 94 95 98.95%
Totals Coverage Status
Change from base Build 23216956868: -0.09%
Covered Lines: 1008
Relevant Lines: 1013

💛 - Coveralls

@nstarman nstarman force-pushed the mnt/py314 branch 2 times, most recently from 99374d0 to baee207 Compare November 24, 2025 00:03
@nstarman
Copy link
Copy Markdown
Collaborator Author

@wesselb I don't know how to resolve this serious problem.

@nstarman
Copy link
Copy Markdown
Collaborator Author

Looking at https://github.com/python/cpython/blob/425f24e4fad672c211307a9f0018c8d39c4db9de/Objects/unionobject.c#L279 I kind of doubt the union alias functionality can continue to work this way.
A potential alternative would be if beartype could dynamically generate TypeAliasType of the union and swap it in-place by modifying the function's annotations.

Meanwhile can I make this a no-op for Python 3.14+?

@wesselb
Copy link
Copy Markdown
Member

wesselb commented Nov 24, 2025

Meanwhile can I make this a no-op for Python 3.14+?

Absolutely. Let's do this for now until we figure out whether this is still possible for 3.14+.

@nstarman
Copy link
Copy Markdown
Collaborator Author

Fully functional until py3.14 then they turn into deprecated no-ops.

@nstarman
Copy link
Copy Markdown
Collaborator Author

Tests need adjustment

@nstarman
Copy link
Copy Markdown
Collaborator Author

I can work on this after #179

@wesselb
Copy link
Copy Markdown
Member

wesselb commented Dec 18, 2025

@nstarman that would be amazing! :)

@nstarman nstarman force-pushed the mnt/py314 branch 10 times, most recently from 23b77fd to 76e1a75 Compare February 13, 2026 04:34
@nstarman nstarman changed the title test: py3.14 enable py3.14 Feb 13, 2026
@nstarman
Copy link
Copy Markdown
Collaborator Author

CleanShot 2026-02-19 at 11 29 56

@nstarman nstarman marked this pull request as ready for review February 19, 2026 16:30
@nstarman nstarman requested review from Copilot and wesselb February 19, 2026 16:30
@nstarman nstarman changed the title enable py3.14 Enable py3.14 and deprecate some alias functionality Feb 19, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Enables Python 3.14 support by avoiding typing.Union monkeypatching (now immutable) and shifting union-alias handling to Plum’s own formatting/registry, with updated tests/docs/CI to cover the new behavior.

Changes:

  • Add Python 3.14 test coverage and split union-alias tests by Python version.
  • Update union aliasing internals to use TypeAliasType on 3.14+ and apply alias-aware formatting in repr_short/repr_type.
  • Adjust dependency constraints and documentation examples for the new 3.14+ behavior.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/plum/_alias.py Adds 3.14+ path using TypeAliasType, deprecates activation APIs, and introduces _transform_union_alias.
src/plum/repr.py Uses _transform_union_alias so repr_short/repr_type can display registered union aliases.
src/plum/_signature.py Refactors beartype type-hint wrapping usage (import/usage cleanup).
src/plum/_type.py Small refactor in resolve_type_hint and adjusts UNION_TYPES container type.
tests/test_alias_upto313.py Skips these tests on 3.14+ and routes activation calls via plum.*.
tests/test_alias_314plus.py New 3.14+ focused tests for alias formatting and deprecated APIs.
tests/test_util.py Updates expected typing.Union[...] short repr for 3.14+.
tests/conftest.py Adds an autouse fixture to isolate/clear the union-alias registry per test.
docs/union_aliases.md Adds version-gated examples for the changed Union repr behavior in 3.14+.
docs/comparison.md Adds version-gated example output to match 3.14+ Union repr changes.
pyproject.toml Adds conditional beartype minimum versions keyed on Python version.
.github/workflows/ci.yml Adds Python 3.14 to CI and pre-release beartype test runs.
.pre-commit-config.yaml Removes default Python language version pinning.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/ci.yml Outdated
Comment thread docs/union_aliases.md Outdated
Comment thread docs/union_aliases.md Outdated
Comment thread tests/test_alias_314plus.py Outdated
Comment thread src/plum/_alias.py Outdated
@nstarman
Copy link
Copy Markdown
Collaborator Author

nstarman commented Feb 19, 2026

@leycec one part of this PR is to change the type hint repr machinery in plum.alias to use TypeAliasType objects that are never used a runtime, only for pretty printing. I was wondering if this functionality, which turns out to be quite simple, would be relevant to upstream into beartype itself. With these aliases type annotations can become visually much simpler.

#225 (comment)

@nstarman
Copy link
Copy Markdown
Collaborator Author

@wesselb, currently this PR uses the TypeAliasType repr directly, but we might want to preserve the old behaviour of transforming it to Union[{repr}]. LMK.

@leycec
Copy link
Copy Markdown
Member

leycec commented Feb 20, 2026

Ho, ho, ho. I have no what idea what's happening here. Yet, I love to see it. @wesselb's Plum is alive and well! You've even attracted a tightly-knit coterie of like-minded Plumthynistas voluntarily maintaining and building out functionality for the next generation. In the modern amped-up LLM era, you've even got Copilot running circles with automated code review. Wonderful. Simply wonderful.

Since I have no idea what's happening here, I will now begin monologuing from a place of profound ignorance. I didn't even know that the Plum API provided union aliases. Now that I do, they vaguely share more than a passing resemblance to:

  • PEP 695 type aliases. They're a lot more general-purpose than Plum-specific union aliases. They're also standardized, fully supported by @beartype, fully usable under Python ≤ 3.11 via the typing_extensions.TypeAliasType backport, and parametrizeable by arbitrary PEP 484 type variables or PEP 646 type variable tuples. Since this PR references the official typing.TypeAliasType type, everyone here already knows everything about PEP 695. Thus, I ask you: "In 2026, can't Plum users just use type aliases directly? Are Plum-specific union aliases even desired or required anymore?" That is to say:

    # This stuff is all reasonable. So far, so good...
    import numpy as np
    from typing import Union
    from plum import dispatch
    
    scalar_types = tuple(np.sctypeDict.values())  # All NumPy scalar types
    
    # This stuff is non-standard and thus kinda less reasonable. Oh noes!
    from plum import activate_union_aliases, set_union_alias
    activate_union_aliases()
    Scalar = Union[scalar_types]  # Union of all NumPy scalar types
    set_union_alias(Scalar, alias="Scalar")
    
    # This stuff is semantically equivalent to the above and thus reasonable.
    # Oh yeah! You love to see it. Can't Plum users just do stuff like this?
    type Scalar = Union[scalar_types]
    
    # Good ol' @dispatch. Still stomping bugs after all these years. *GET 'EM.*
    @dispatch
    def add(x: Scalar, y: Scalar):
        return x + y
  • @beartype hint_overrides. Like Plum, @beartype too implemented an ad-hoc type hint aliasing scheme years before PEP 695 proudly sauntered into the saloon and kicked over all our hard work. Like Plum-specific union aliases, @beartype-specific hint_overrides may no longer have a reason to exist. Like PEP 695 type aliases, they're general-purpose. Unlike PEP 695 type aliases, they're not parametrizeable by arbitrary PEP 484 type variables or PEP 646 type variable tuples. Like always, PEP 695 type aliases win. Although @beartype still technically supports hint_overrides (mostly just for backward compatibility and because it's now more work to remove support than quietly maintain it), the @beartype userbase basically just uses type aliases these days. It doesn't help that the hint_overrides API is... more than a little awkward. Since Python lacks an official frozendict type, the API requires use of a @beartype-specific FrozenDict type embedded within a BeartypeConf passed to the @beartype decorator. Parens Hell™ is what I'm reluctantly saying. For completeness, this Unhinged API from Parens Hell™ resembles:

    from beartype, BeartypeConf, FrozenDict
    
    # If you're not vomiting already, you will be. You will be.
    @beartype(conf=BeartypeConf(hint_overrides=FrozenDict({Union[scalar_types]: 'Scalar'})))
    def add(x: Scalar, y: Scalar):
        return x + y

Of course, the latter example isn't quite right. @beartype-specific hint_overrides were designed to dynamically replace all instances of one type hint with another. In the case of Plum-specific union aliases, though, you don't actually want to replace type hints; you just want unreadable type hints to be reduced to readable aliases. A new BeartypeConf option (say, hint_labels) whose value is a similar FrozenDict data structure could be added to transparently address that.

I could keep typing, but probably shouldn't. PEP 695 type aliases exist. They appear to do everything everybody wants. Does anybody actually want non-standard stuff like Plum's union aliases or @beartype's hint_overrides anymore? If so, why? Is there something we still do that type aliases don't? I stroke my beard, squint my eyes, and arch an eyebrow suggestively.


if only i too had a beautiful uniform like this

@nstarman
Copy link
Copy Markdown
Collaborator Author

nstarman commented Feb 22, 2026

Thus, I ask you: "In 2026, can't Plum users just use type aliases directly? Are Plum-specific union aliases even desired or required anymore?" That is to say:

This PR deprecates activate_union_aliases, but keeps set_union_alias, using dynamically generated PEP 695 type aliases to enable (in repr only, not during type-checking) the alias name to be displayed instead of the union. If one has control over the creation of a function, yes, direct use of type aliases are to be preferred:

scalar_types = tuple(np.sctypeDict.values())  # All NumPy scalar types

type Scalar = Union[scalar_types]

@dispatch
def add(x: Scalar, y: Scalar):
    return x + y

But these aliases are "local" to the context they are used. With set_union_alias we can make it so that any type annotations that plum displays will use the alias, which is especially useful for functions we do not control:

# imaginary_library (I don't control)

two_elt_dtype = np.dtype(...)

def add(x: Fraction | tuple[float | np.floating, float | np.floating] | two_elt_dtype, y: Fraction | tuple[float | np.floating, float | np.floating] | two_elt_dtype) -> Fraction:
    return Fraction(x) + Fraction(y)


# my_library (I do control)

plum.dispatch(imaginary_library.add)  # register in their method

type Scalar = Union[scalar_types]  # love it

@plum.dispatch
def add(x: Scalar, y: Scalar) -> Scalar:  # register in our method
    return x + y

# IDE
>>> add.methods
List of X method(s):
    [0] add(x: Scalar, y: Scalar] -> Scalar: ...
    [1] add(x: Fraction | tuple[float | np.floating, float | np.floating] | two_elt_dtype, y: Fraction | tuple[float | np.floating, float | np.floating] | two_elt_dtype) -> Fraction: ...
    
>>> plum.set_union_alias(Fraction | tuple[float | np.floating, float | np.floating] | two_elt_dtype, "FractionLike")
>>> add.methods
List of X method(s):
    [0] add(x: Scalar, y: Scalar] -> Scalar: ...
    [1] add(x: FractionLike, y: FractionLike) -> Fraction: ...

plum.set_union_alias makes Boim's life easier when examining plum dispatches for methods he didn't define (without actually interfering with those methods). In beartype, I'm suggesting that the diagnostic printouts might similarly benefit — grossly long type annotations can be condensed to a convenient alias without actually mutating the code.

A new BeartypeConf option (say, hint_labels) whose value is a similar FrozenDict data structure could be added to transparently address that.

That sounds like a good application.
Then here in plum we could get rid of all the alias machinery, and just give instructions about how to use beartype.
I do kind of like making it immutable (and then configurable by some pyproject.toml-able setting here in plum or perhaps in beartype).

Ideally Python itself would give a means to herd unruly type annotations into formation, e.g. in pprint, but I don't see that happening any time soon.

@nstarman
Copy link
Copy Markdown
Collaborator Author

This PR deprecates activate_union_aliases,

@wesselb we could actually keep activate_union_aliases and deactivate_union_aliases alive, which might be useful for debugging if someone wants to destructure a union alias and see the full union type when examining a Function.methods.

@wesselb
Copy link
Copy Markdown
Member

wesselb commented Mar 6, 2026

Hey @nstarman and @leycec! Apologies for the radio silence. Work's been really busy.

@nstarman got this exactly right! The example of add.methods with Scalar is a very relevant one. (To add to this, I have a project LAB that I work on, and the method signatures there become extremely unwieldy without type aliasing. This project was the original motivation for union aliases.)

@nstarman, thanks a lot for putting together this PR. It looks really, really good. I'll do an in-depth review ASAP. (I won't be so slow this time!)

@wesselb, currently this PR uses the TypeAliasType repr directly, but we might want to preserve the old behaviour of transforming it to Union[{repr}]. LMK.

I think I like the new behaviour, so I'd be happy to keep it the way you implemented it now. What do you think?

@wesselb we could actually keep activate_union_aliases and deactivate_union_aliases alive, which might be useful for debugging if someone wants to destructure a union alias and see the full union type when examining a Function.methods.

That is a good suggestion. Let's do that!

nstarman added 5 commits March 6, 2026 11:51
Signed-off-by: nstarman <nstarman@users.noreply.github.com>
Signed-off-by: nstarman <nstarman@users.noreply.github.com>
Signed-off-by: nstarman <nstarman@users.noreply.github.com>
Signed-off-by: nstarman <nstarman@users.noreply.github.com>
Signed-off-by: nstarman <nstarman@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/plum/_alias.py Outdated
args_set = set(args)
# Look for a matching alias in the registry
for union_args, type_alias in _ALIASED_UNIONS.items():
if set(union_args) == args_set:
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

On Python < 3.14, _ALIASED_UNIONS maps to str aliases, but _transform_union_alias() blindly returns the dict value. When this is called from repr_type() it will turn a union into a plain string and Rich will render it with quotes (e.g. 'IntStr') instead of a type representation. Consider making _transform_union_alias() only return when the stored value is a TypeAliasType (or make the <3.14 registry store TypeAliasTypes), and otherwise leave x unchanged for the legacy monkeypatch-based path.

Suggested change
if set(union_args) == args_set:
# On Python < 3.14, the registry may store string aliases; only
# return actual TypeAliasType instances to avoid turning the union
# into a plain string for representation.
if set(union_args) == args_set and isinstance(type_alias, TypeAliasType):

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I'm not sure this is correct. The tests all pass.
This is a subtle one.
Ping @wesselb

Comment thread docs/union_aliases.md Outdated
Comment thread docs/union_aliases.md Outdated
Comment thread docs/comparison.md
Copy link
Copy Markdown
Member

@wesselb wesselb left a comment

Choose a reason for hiding this comment

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

This is great, @nstarman! Thanks so much for putting this together. I've left an array of minor suggestions to keep the style in sync with the rest of the code. I hope you don't mind.

If I understand correctly, the algorithm for TypeAliasType checks if a union occurs in _ALIASED_UNIONS and replaces it with the alias if there is a match. This works if the union is exactly contained in _ALIASED_UNIONS, but doesn't if it is a union of two alias unions or if there are other types in the union too.

For example, (str, Scalar) is expanded to (str, float, int, Number, ...), which would ideally be aliased back to (str, Scalar). The case of the if-statement for before Python 3.14 has some logic to handle the general case. Do you think it would be possible to do this for the Python 3.14 and later case too?

I'm happy to jump in and hack around myself to see if I can make this work. It seems like the logic to determine which aliases are contained in a union could possibly be shared.

Comment thread docs/union_aliases.md Outdated
Comment thread docs/union_aliases.md Outdated
Comment thread docs/union_aliases.md Outdated
Comment thread docs/union_aliases.md Outdated
Comment thread docs/union_aliases.md Outdated
Comment thread tests/test_alias_314plus.py Outdated
Comment thread tests/test_alias_314plus.py Outdated
Comment thread tests/test_alias_314plus.py Outdated
Comment thread tests/test_alias_314plus.py Outdated
Comment thread tests/test_alias_upto313.py Outdated
Comment thread src/plum/_alias.py Outdated
Comment thread tests/test_alias_314plus.py Outdated
Comment thread tests/test_alias_314plus.py Outdated
Co-authored-by: Wessel <wessel.p.bruinsma@gmail.com>
Co-authored-by: Nathaniel Starkman <nstarman@users.noreply.github.com>
Signed-off-by: nstarman <nstarman@users.noreply.github.com>
@nstarman
Copy link
Copy Markdown
Collaborator Author

If I understand correctly, the algorithm for TypeAliasType checks if a union occurs in _ALIASED_UNIONS and replaces it with the alias if there is a match. This works if the union is exactly contained in _ALIASED_UNIONS, but doesn't if it is a union of two alias unions or if there are other types in the union too.

I agree that would be a nice upgrade.

I'm happy to jump in and hack around myself to see if I can make this work. It seems like the logic to determine which aliases are contained in a union could possibly be shared.

That would be great!

@nstarman
Copy link
Copy Markdown
Collaborator Author

nstarman commented Apr 6, 2026

Ping @wesselb :)

@wesselb
Copy link
Copy Markdown
Member

wesselb commented Apr 9, 2026

Apologies, @nstarman! Last couple of months have been very busy. Perhaps we could also merge the current approach for now and open an issue to extend it to more general unions. How would that sound?

@nstarman
Copy link
Copy Markdown
Collaborator Author

nstarman commented Apr 9, 2026

SGTM. It should only make repr changes, not actual dispatch changes, so most people won't notice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Alternative for Union Aliases in Py3.14+ Problems with Union Aliases in Python 3.14

5 participants