Skip to content

Conversation

@provinzkraut
Copy link
Contributor

@provinzkraut provinzkraut commented Nov 29, 2025

Fix #957.

This is a few fixes combined into one, since they were tightly coupled.

Handling of "new style" generics during type resolution

When subscribing a "new style" generic (such as collections.abc.Mapping), it produces a types.GenericAlias (vs. the "old style" typing._GenericAlias), which msgspec did not handle correctly during inspection

Handling of type var syntax

When dealing with builtin generics that resolve to typing.TypeAlias, msgspec did not account for type var syntax at correctly in all cases, so type information would get lost during the conversion process

Type conversions on types.GenericAlias

During type conversion, msgspec caches certain information on the type objects themselves, if the types are complex (i.e. Structs or dataclass-like).

When decoding into a Foo[int], msgspec will set an __msgspec_cache__ attribute on the Foo[int] alias type.

For typing._GenericAlias, this work, since it has a __dict__, so you can just assign attributes to it. However, types.GenericAlias does not allow assigning arbitrary attributes to it.

I fix this by downtyping types.GenericAlias into a typing._GenericAlias, when encountering a generic Struct or dataclass type. This allows to keep the existing caching mechanism in place.

This seemed like the most reasonable fix to me, as the other alternatives would like incur some sort of performance penalty; By storing the typing info directly on the alias, msgspec can forego maintaining a dedicated cache, making lookups very fast. It also allows to not care about invalidating a cache, since it will just be gce'd when the alias isn't referenced anymore.

One thing to not here though is that in the future, typing._GenericAlias might just go away (at least from the stdlib), in which case we'll have to find another way to deal with this.

@provinzkraut provinzkraut force-pushed the fix-tvar-typing-generic-alias branch from 63b9a18 to 57bf7e6 Compare November 29, 2025 14:06
@provinzkraut
Copy link
Contributor Author

provinzkraut commented Nov 30, 2025

Okay, maybe this isn't a good solution. I've discovered a slight issue with the typing._GenericAlias implementation:

It does not cache itself when called directly (i.g. doing _GenericAlias(<type>, <args>), but only when subscribed. This means that List[int] is List[int] holds true, but _GenericAlias(List, int) is _GenericAlias(List, int) does not.

Since msgspec relies on that cache being preserved, the solution as currently proposed only maintains the cache within a decoder instance, meaning that calling decode() twice, even with the same type, does not maintain the cache.

Note that this only affects generic types bound by a types.GenericAlias. Everything else is unaffected, including "regular" generic classes (e.g. a class Foo[T](Struct) will be cached just fine.

@provinzkraut provinzkraut force-pushed the fix-tvar-typing-generic-alias branch from 43d8d35 to 4285b96 Compare November 30, 2025 11:53
@provinzkraut
Copy link
Contributor Author

provinzkraut commented Nov 30, 2025

Alright, I think I managed to find a solution. Essentially, we're now reproducing a typing._GenericAlias that properly caches itself, from a types.GenericAlias.

It's basically doing

typing._GenericAlias(
    alias.__origin__, 
    alias.__origin__.__parameters__
).__getitem__(*alias.__args__)

which is functionally the same as

alias = typing._SpecialGenericAlias(list, 1)[int]
# this is the same as
alias = typing.List[int]

I've also added some more test cases to ensure the caching works properly in all newly supported cases.

@provinzkraut
Copy link
Contributor Author

Another friendly ping to @ofek for a review :)
Hope you don't mind, I just didn't want these PRs to get buried, but I also don't want to continue to send PRs and have them pile up 😅
LMK if I can do something to make the review process easier for you.

@ofek
Copy link
Collaborator

ofek commented Dec 19, 2025

I've been really busy, sorry for the wait! Can you confirm locally that the minor performance regression here compared to the last run on main is just noise?

I really appreciate all the work you've been doing 🙏

@provinzkraut
Copy link
Contributor Author

provinzkraut commented Dec 19, 2025

Can you confirm locally that the minor performance regression here compared to the last run on main is just noise?

I'll check it out!

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.

Types inheriting from GenericAlias types (such as Mapping) cannot be decoded.

2 participants