Python: distinguish type[X] class objects from instances in ty attribution#7933
Merged
knutwannheden merged 1 commit intoJun 7, 2026
Merged
Conversation
…y attribution A value of type type[X] (a class object) was given the exact same JavaType as an instance of X, so the LST could not tell class access from instance access. ty emits this as a subclassOf descriptor (base = X), which the parser resolved straight to its base X. Model type[X] (and PEP 747 TypeForm[X]) as a JavaType.Parameterized over type with X as its sole type parameter, mirroring list[X]. is_assignable_to(X, type[X]) is now False (raw name is type, not X) while the wrapped X stays recoverable via type_parameters[0] for attribute/classmethod resolution. The declaring-type path still resolves through to X so members reached via the class object remain attributed to X.
type[X] class objects from instances in ty attributiontype[X] class objects from instances in ty attribution
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
In rewrite-python's ty-based type attribution, a value of type
type[X](a class object) was given the exact sameJavaTypeas an instance ofX, so class-vs-instance access was indistinguishable in the LST.ty-types emits
type[X]as asubclassOfdescriptor whosebaseisX. The parser resolved that straight to its baseX, so a parameterc: type[M]and a parameterinst: Mended up with the identicalJavaType.ClassforM. Consequentlyis_assignable_to("…M", type_of(c))returnedTrueeven thoughcis a class object, not anMinstance.This breaks any recipe that must tell a class from an instance. It surfaced while validating the Pydantic recipes against
pydantic/pydantic-settings:ReplaceModelFieldsInstanceAccessrewrites a deprecated instance accessobj.model_fields→type(obj).model_fields, gated onis_assignable_to("pydantic.main.BaseModel", receiver.type). Becausetype[X]collapsed toX, it rewrote class access such asinto
type(settings_cls).model_fields— which reaches the metaclass and breaks at runtime.Examples
is_assignable_to("M", t)is_assignable_to("type", t)inst(M)JavaType.Class MTrueFalsec(type[M])Parameterized(type, [M])False(wasTrue)TrueThe wrapped class
Mstays recoverable viatype_parameters[0], so attributes/classmethods reached through the class object still resolve, and the call's declaring type remainsM.Summary
type[X](ty'ssubclassOf X) now resolves to aJavaType.ParameterizedovertypewithXas its sole type parameter — mirroring howlist[X]is modelled — instead of collapsing toX. New helperPythonTypeMapping._make_class_object_type.TypeForm[X]is given the same class-object representation (aTypeForm[X]value is the typeX, not an instance ofX), keeping the two paths consistent._declaring_type_from_descriptor) is unchanged: it still resolvessubclassOf/typeFormthrough to the underlying class, so a member reached through atype[X]value is attributed toX(where it is declared), not to thetypewrapper.is_assignable_toneeded no changes: becauseParameterized.fully_qualified_namereturns the raw base nametype,is_assignable_to("M", type[M])isFalseandis_assignable_to("type", type[M])isTrueout of the box.Test plan
New unit tests (mock descriptors, no CLI) in
TestSubclassOfDescriptor:type[M]→Parameterized(type, [M]); not assignable toM; assignable totype; instance ofMstill assignable toM; missingbase→Unknown; declaring-type path still resolves toM.Updated
TestTypeFormDescriptor/TestTypeFormIntegrationto assert the new class-object representation.New end-to-end tests in
TestClassObjectTypeAttribution(first-party only, no deps):type[M]param not assignable toMbut assignable totype; wrapped class recoverable; classmethod reached viatype[M]resolves toM; classmethodclsunchanged.uv run --extra dev pytest tests/python/test_type_attribution.py— 140 passed.uv run --extra dev pytest tests/recipes tests/python/all/tree— 455 passed (no regressions).Independent of the dependency-path (Python: accept a caller-provisioned dependency path for
tytype attribution #7930) and multi-file-supertype work; based onmain.