Skip to content

Fix PEP 695 generic alias parameters being bound by position - #1275

Open
vmphase wants to merge 13 commits into
msgspec:mainfrom
vmphase:fix/pep695-alias-param-binding
Open

vmphase wants to merge 13 commits into
msgspec:mainfrom
vmphase:fix/pep695-alias-param-binding

Conversation

@vmphase

@vmphase vmphase commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Generic type aliases bind their arguments by position in the alias body instead of by declared parameter order. The body's __parameters__ follow first appearance, so for type Swap[A, B] = dict[B, A] the arguments end up swapped:

import msgspec
import msgspec.inspect as mi

type Swap[A, B] = dict[B, A]
dec = msgspec.json.Decoder(Swap[int, str])
print(mi.type_info(Swap[int, str]))
print(dec.decode(b'{"x": 1}'))

# on upstream this resolves to dict[int, str] and fails:
"""
DictType(key_type=IntType(...), value_type=StrType(...), ...)
msgspec.ValidationError: Expected `int`, got `str` - at `key` in `$`

DictType(key_type=StrType(...), value_type=IntType(...), ...)
{'x': 1}
"""

The same happens with ParamSpec and TypeVarTuple parameters whenever the declared order differs from the body's:

type D[T, **P] = Callable[P, T]
D[int, [str]]  # upstream: TypeError, bound as (P, T)

type Last[T, *Ts] = tuple[*Ts, T]
Last[int, str, float]  # upstream: tuple[int, str, float], should be tuple[str, float, int]

^ it now goes through _resolve_type_alias, which binds by name, applies PEP 696 defaults, and raises TypeError on too many / few arguments.

@sobolevn sobolevn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

please, improve test coverage: more paramspec and typevartuple types :)

Comment thread tests/unit/test_inspect.py Outdated
@vmphase
vmphase force-pushed the fix/pep695-alias-param-binding branch from 6b93bab to afc37ad Compare October 16, 2026 11:08
@vmphase

vmphase commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

please, improve test coverage: more paramspec and typevartuple types :)

The positional fallback for ParamSpec/TypeVarTuple aliases had the original bug (as I noted in PR description before) and I was too lazy to change their behavior that day. However, since you asked for tests, I got my act together and replaced it with binding by declared parameter for all kinds =)

Comment thread src/msgspec/_core.c Outdated
Comment thread src/msgspec/_utils.py
return has_default is not None and has_default()


def _resolve_type_alias(alias, args):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Question: can we re-use some parts of typing.py? this looks like a complete rewrite from scratch

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hmm, I guess we can at some point. The reason I wrote it from scratch is that it was easier to debug to me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Well, after a few hours I couldn't think of anything better than reusing typing's __typing_prepare_subst__ for binding and our existing _apply_params for substitution. IIRC there is no public api for resolving aliases, and I am not familiar with private ones if they do exist.

This branch has not been deployed

No deployments
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.

2 participants