Skip to content

Comments

[ty] Add precise inference for indexing, slicing and unpacking NamedTuple instances#19560

Merged
AlexWaygood merged 7 commits intomainfrom
alex-brent/namedtuples
Aug 13, 2025
Merged

[ty] Add precise inference for indexing, slicing and unpacking NamedTuple instances#19560
AlexWaygood merged 7 commits intomainfrom
alex-brent/namedtuples

Conversation

@AlexWaygood
Copy link
Member

@AlexWaygood AlexWaygood commented Jul 25, 2025

Summary

Infer the correct tuple supertype for NamedTuple classes. This allows us to infer precise types for indexing into NamedTuple classes, unpacking NamedTuple classes, and comparing NamedTuple classes.

from typing import NamedTuple

class Foo(NamedTuple):
    x: int
    y: str

def _(f: Foo):
    reveal_type(f[0])  # revealed: int
    reveal_type(f[1])  # revealed: str
    a, b = f
    reveal_type(a)  # revealed: int
    reveal_type(b)  # revealed: str

Test Plan

Mdtests.

Typing conformance diff

This is all great! We see lots of type-assertion-failures going away, and lots of diagnostics now being emitted on lines in the conformance tests that are meant to produce type-checker errors.

Ecosystem analysis

This all looks good to me! Analysis in #19560 (comment).

New ecosystem panics

Unfortunately this causes many new ecosystem panics, because packaging has a NamedTuple class that has a field annotated with a recursive type alias: https://github.com/pypa/packaging/blob/0055d4b8ff353455f0617690e609bc68a1f9ade2/src/packaging/_parser.py#L55. The initial support #19805 added for recursive types doesn't help us here, because it only adds support for explicit PEP-695 type aliases, whereas packaging's type alias is an implicit one.

Many projects use packaging, so this unfortunately causes quite a big fallout...


Co-authored-by: Brent Westbrook [email protected]

@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Jul 25, 2025
@AlexWaygood AlexWaygood force-pushed the alex-brent/namedtuples branch 3 times, most recently from 201d595 to 92bb82c Compare July 25, 2025 18:06
@github-actions
Copy link
Contributor

github-actions bot commented Jul 25, 2025

mypy_primer results

Changes were detected when running on open source projects
attrs (https://github.com/python-attrs/attrs)
+ src/attr/_make.py:703:29: error[unresolved-attribute] Type `Attribute` has no attribute `name`
+ src/attr/_make.py:705:50: error[not-iterable] Object of type `type` is not iterable
+ src/attr/_make.py:739:22: error[not-iterable] Object of type `type` is not iterable
+ tests/test_make.py:197:52: error[not-iterable] Object of type `type` is not iterable
+ tests/test_make.py:222:25: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `type`
+ tests/test_make.py:223:54: error[not-iterable] Object of type `type` is not iterable
+ tests/test_make.py:268:20: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `type`
+ tests/test_make.py:271:18: error[not-iterable] Object of type `type` is not iterable
+ tests/test_make.py:287:20: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `type`
+ tests/test_make.py:290:18: error[not-iterable] Object of type `type` is not iterable
- Found 650 diagnostics
+ Found 660 diagnostics

pydantic (https://github.com/pydantic/pydantic)
+ pydantic/v1/types.py:704:12: error[unsupported-operator] Operator `>=` is not supported for types `str` and `int`, in comparing `int | Literal["n", "N", "F"]` with `Literal[0]`
+ pydantic/v1/types.py:706:22: error[unsupported-operator] Operator `+` is unsupported between objects of type `int` and `int | Literal["n", "N", "F"]`
+ pydantic/v1/types.py:714:20: error[invalid-argument-type] Argument to function `abs` is incorrect: Expected `SupportsAbs[Unknown]`, found `int | Literal["n", "N", "F"]`
+ pydantic/v1/types.py:715:41: error[invalid-argument-type] Argument to function `abs` is incorrect: Expected `SupportsAbs[Unknown]`, found `int | Literal["n", "N", "F"]`
+ pydantic/v1/types.py:715:41: error[invalid-argument-type] Argument to function `abs` is incorrect: Expected `SupportsAbs[Unknown]`, found `int | Literal["n", "N", "F"]`
+ pydantic/v1/types.py:718:32: error[invalid-argument-type] Argument to function `abs` is incorrect: Expected `SupportsAbs[Unknown]`, found `int | Literal["n", "N", "F"]`
- Found 757 diagnostics
+ Found 763 diagnostics

mongo-python-driver (https://github.com/mongodb/mongo-python-driver)
- bson/decimal128.py:103:50: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 511 diagnostics
+ Found 510 diagnostics

cloud-init (https://github.com/canonical/cloud-init)
+ cloudinit/config/modules.py:287:48: error[unresolved-attribute] Type `ModuleType` has no attribute `handle`
+ cloudinit/config/modules.py:298:39: error[unresolved-attribute] Type `ModuleType` has no attribute `handle`
- Found 614 diagnostics
+ Found 616 diagnostics

pywin32 (https://github.com/mhammond/pywin32)
+ win32/Demos/win32gui_menu.py:354:16: error[unsupported-operator] Operator `&` is unsupported between objects of type `int | None` and `Literal[8]`
+ win32/test/test_win32guistruct.py:89:43: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `str | None`
- Found 2004 diagnostics
+ Found 2006 diagnostics

psycopg (https://github.com/psycopg/psycopg)
- psycopg/psycopg/pq/pq_ctypes.py:937:71: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 655 diagnostics
+ Found 654 diagnostics

pytest (https://github.com/pytest-dev/pytest)
- src/_pytest/hookspec.py:1114:6: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `TestShortLogReport | tuple[str, str, str | tuple[str, Mapping[str, bool]]]`
+ src/_pytest/hookspec.py:1114:6: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `tuple[str, str, str | tuple[str, Mapping[str, bool]]]`

sympy (https://github.com/sympy/sympy)
+ sympy/integrals/manualintegrate.py:1894:28: error[unsupported-operator] Operator `<` is not supported for types `Basic` and `int`, in comparing `Unknown | Basic` with `Literal[0]`
+ sympy/integrals/manualintegrate.py:1906:49: error[invalid-argument-type] Argument is incorrect: Expected `Expr`, found `Unknown | Basic`
- Found 12989 diagnostics
+ Found 12991 diagnostics
Memory usage changes were detected when running on open source projects
flake8 (https://github.com/pycqa/flake8)
-     memo fields = ~54MB
+     memo fields = ~57MB

@github-actions

This comment was marked as outdated.

@AlexWaygood
Copy link
Member Author

The ecosystem report indicates that we shouldn't do this until we support unpacking tuple subclasses in the same way as we support unpacking tuples.

@AlexWaygood AlexWaygood force-pushed the alex/tuple-getitem branch 2 times, most recently from e9b6681 to 7961ecd Compare July 29, 2025 17:37
Base automatically changed from alex/tuple-getitem to main July 30, 2025 11:25
@AlexWaygood AlexWaygood force-pushed the alex-brent/namedtuples branch from 92bb82c to c9282cb Compare July 30, 2025 12:11
@github-actions
Copy link
Contributor

github-actions bot commented Jul 30, 2025

Diagnostic diff on typing conformance tests

Changes were detected when running ty on typing conformance tests
--- old-output.txt	2025-08-13 15:17:32.371829275 +0000
+++ new-output.txt	2025-08-13 15:17:32.438829393 +0000
@@ -1,5 +1,5 @@
 WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
-fatal[panic] Panicked at /home/runner/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/918d35d/src/function/execute.rs:215:25 when checking `/home/runner/work/ruff/ruff/typing/conformance/tests/aliases_typealiastype.py`: `infer_definition_types(Id(c91d)): execute: too many cycle iterations`
+fatal[panic] Panicked at /home/runner/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/918d35d/src/function/execute.rs:215:25 when checking `/home/runner/work/ruff/ruff/typing/conformance/tests/aliases_typealiastype.py`: `infer_definition_types(Id(14cb3)): execute: too many cycle iterations`
 _directives_deprecated_library.py:15:31: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int`
 _directives_deprecated_library.py:30:26: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `str`
 _directives_deprecated_library.py:36:41: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `Self@__add__`
@@ -661,14 +661,8 @@
 literals_semantics.py:24:5: error[invalid-assignment] Object of type `Literal[0]` is not assignable to `Literal[False]`
 literals_semantics.py:25:5: error[invalid-assignment] Object of type `Literal[False]` is not assignable to `Literal[0]`
 literals_semantics.py:33:5: error[invalid-assignment] Object of type `Literal[6, 7, 8]` is not assignable to `Literal[3, 4, 5]`
-namedtuples_define_class.py:23:1: error[type-assertion-failure] Argument does not have asserted type `int`
-namedtuples_define_class.py:24:1: error[type-assertion-failure] Argument does not have asserted type `int`
-namedtuples_define_class.py:25:1: error[type-assertion-failure] Argument does not have asserted type `str`
-namedtuples_define_class.py:26:1: error[type-assertion-failure] Argument does not have asserted type `str`
-namedtuples_define_class.py:27:1: error[type-assertion-failure] Argument does not have asserted type `int`
-namedtuples_define_class.py:28:1: error[type-assertion-failure] Argument does not have asserted type `int`
-namedtuples_define_class.py:29:1: error[type-assertion-failure] Argument does not have asserted type `tuple[int, int]`
-namedtuples_define_class.py:30:1: error[type-assertion-failure] Argument does not have asserted type `tuple[int, int, str]`
+namedtuples_define_class.py:32:7: error[index-out-of-bounds] Index 3 is out of bounds for tuple `Point` with length 3
+namedtuples_define_class.py:33:7: error[index-out-of-bounds] Index -4 is out of bounds for tuple `Point` with length 3
 namedtuples_define_class.py:44:6: error[missing-argument] No argument provided for required parameter `y`
 namedtuples_define_class.py:45:6: error[missing-argument] No argument provided for required parameter `y`
 namedtuples_define_class.py:46:15: error[invalid-argument-type] Argument is incorrect: Expected `int`, found `Literal[""]`
@@ -679,15 +673,13 @@
 namedtuples_define_class.py:95:1: error[type-assertion-failure] Argument does not have asserted type `int | float`
 namedtuples_define_class.py:96:1: error[type-assertion-failure] Argument does not have asserted type `int | float`
 namedtuples_define_class.py:98:19: error[invalid-argument-type] Argument is incorrect: Expected `str`, found `float`
-namedtuples_usage.py:27:1: error[type-assertion-failure] Argument does not have asserted type `int`
-namedtuples_usage.py:28:1: error[type-assertion-failure] Argument does not have asserted type `int`
-namedtuples_usage.py:29:1: error[type-assertion-failure] Argument does not have asserted type `str`
-namedtuples_usage.py:30:1: error[type-assertion-failure] Argument does not have asserted type `str`
-namedtuples_usage.py:31:1: error[type-assertion-failure] Argument does not have asserted type `int`
-namedtuples_usage.py:32:1: error[type-assertion-failure] Argument does not have asserted type `int`
+namedtuples_type_compat.py:22:1: error[invalid-assignment] Object of type `Point` is not assignable to `tuple[int, int]`
+namedtuples_type_compat.py:23:1: error[invalid-assignment] Object of type `Point` is not assignable to `tuple[int, str, str]`
+namedtuples_usage.py:34:7: error[index-out-of-bounds] Index 3 is out of bounds for tuple `Point` with length 3
+namedtuples_usage.py:35:7: error[index-out-of-bounds] Index -4 is out of bounds for tuple `Point` with length 3
 namedtuples_usage.py:41:1: error[invalid-assignment] Cannot assign to object of type `Point` with no `__setitem__` method
-namedtuples_usage.py:49:1: error[type-assertion-failure] Argument does not have asserted type `int`
-namedtuples_usage.py:50:1: error[type-assertion-failure] Argument does not have asserted type `str`
+namedtuples_usage.py:52:1: error[invalid-assignment] Too many values to unpack: Expected 2
+namedtuples_usage.py:53:1: error[invalid-assignment] Not enough values to unpack: Expected 4
 narrowing_typeguard.py:17:9: error[type-assertion-failure] Argument does not have asserted type `tuple[str, str]`
 narrowing_typeguard.py:32:9: error[type-assertion-failure] Argument does not have asserted type `set[int]`
 narrowing_typeguard.py:69:9: error[type-assertion-failure] Argument does not have asserted type `int`
@@ -868,5 +860,5 @@
 typeddicts_operations.py:60:1: error[type-assertion-failure] Argument does not have asserted type `str | None`
 typeddicts_type_consistency.py:101:1: error[invalid-assignment] Object of type `Unknown | None` is not assignable to `str`
 typeddicts_usage.py:40:24: error[invalid-type-form] The special form `typing.TypedDict` is not allowed in type expressions. Did you mean to use a concrete TypedDict or `collections.abc.Mapping[str, object]` instead?
-Found 869 diagnostics
+Found 861 diagnostics
 WARN A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details.

@AlexWaygood AlexWaygood force-pushed the alex-brent/namedtuples branch from c9282cb to a79d285 Compare August 5, 2025 18:19
@AlexWaygood AlexWaygood changed the base branch from main to alex/remove-tuples August 5, 2025 18:19
@AlexWaygood AlexWaygood force-pushed the alex-brent/namedtuples branch from a79d285 to b7e96a7 Compare August 5, 2025 18:21
@AlexWaygood AlexWaygood force-pushed the alex/remove-tuples branch 8 times, most recently from 3a8597e to 7ef83cd Compare August 7, 2025 11:54
@AlexWaygood AlexWaygood force-pushed the alex-brent/namedtuples branch from b7e96a7 to 32c5d8e Compare August 7, 2025 11:59
@AlexWaygood AlexWaygood force-pushed the alex/remove-tuples branch 2 times, most recently from 7ad590d to 00b10f8 Compare August 8, 2025 12:00
@AlexWaygood AlexWaygood force-pushed the alex-brent/namedtuples branch from e78cb63 to a904ceb Compare August 11, 2025 13:56
@AlexWaygood AlexWaygood force-pushed the alex-brent/namedtuples branch from a904ceb to e26f607 Compare August 11, 2025 14:41
Base automatically changed from alex/remove-tuples to main August 11, 2025 21:03
@AlexWaygood AlexWaygood force-pushed the alex-brent/namedtuples branch from e26f607 to 017ad8b Compare August 12, 2025 15:10
@AlexWaygood
Copy link
Member Author

AlexWaygood commented Aug 12, 2025

Ecosystem analysis

attrs

These are all because we previously inferred the three variables here as all having type Unknown, because the _transform_attrs function there returns an instance of a NamedTuple class.

+ src/attr/_make.py:703:29: error[unresolved-attribute] Type `Attribute` has no attribute `name`

This is expected with our current capabilities. The Attribute class doesn't declare the name instance attribute as a type annotation, nor does it statically set the name instance attribute in any methods; it's only set dynamically, e.g. here. There is a name entry in the class's __slots__, but we don't support __slots__ for inference of instance attributes currently.

+ src/attr/_make.py:705:50: error[not-iterable] Object of type `type` is not iterable
+ src/attr/_make.py:739:22: error[not-iterable] Object of type `type` is not iterable
+ tests/test_make.py:197:52: error[not-iterable] Object of type `type` is not iterable
+ tests/test_make.py:222:25: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `type`
+ tests/test_make.py:223:54: error[not-iterable] Object of type `type` is not iterable
+ tests/test_make.py:268:20: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `type`
+ tests/test_make.py:271:18: error[not-iterable] Object of type `type` is not iterable
+ tests/test_make.py:287:20: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `type`
+ tests/test_make.py:290:18: error[not-iterable] Object of type `type` is not iterable

these all look like true positives to me? We now infer the attrs variable here as being of type type because it's the first element of this NamedTuple class. Not totally sure what's going on here?

Pydantic

pydantic (https://github.com/pydantic/pydantic)
+ pydantic/v1/types.py:704:12: error[unsupported-operator] Operator `>=` is not supported for types `str` and `int`, in comparing `int | Literal["n", "N", "F"]` with `Literal[0]`
+ pydantic/v1/types.py:706:22: error[unsupported-operator] Operator `+` is unsupported between objects of type `int` and `int | Literal["n", "N", "F"]`
+ pydantic/v1/types.py:714:20: error[invalid-argument-type] Argument to function `abs` is incorrect: Expected `SupportsAbs[Unknown]`, found `int | Literal["n", "N", "F"]`
+ pydantic/v1/types.py:715:41: error[invalid-argument-type] Argument to function `abs` is incorrect: Expected `SupportsAbs[Unknown]`, found `int | Literal["n", "N", "F"]`
+ pydantic/v1/types.py:715:41: error[invalid-argument-type] Argument to function `abs` is incorrect: Expected `SupportsAbs[Unknown]`, found `int | Literal["n", "N", "F"]`
+ pydantic/v1/types.py:718:32: error[invalid-argument-type] Argument to function `abs` is incorrect: Expected `SupportsAbs[Unknown]`, found `int | Literal["n", "N", "F"]`
- Found 759 diagnostics
+ Found 765 diagnostics

Code here. The relevant NamedTuple definition is in typeshed here, and the relevant field annotation is int | Literal["n", "N", "F"]. They're trying to narrow the type to int by doing this:

        if exponent in {'F', 'n', 'N'}:
            raise errors.DecimalIsNotFiniteError()

which isn't a type narrowing operation we support currently.

mongo-python-driver

mongo-python-driver (https://github.com/mongodb/mongo-python-driver)
- bson/decimal128.py:103:50: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 508 diagnostics
+ Found 507 diagnostics

Code here. This is another case of code running into typeshed's annotation for DecimalTuple.exponent.

cloud-init

cloud-init (https://github.com/canonical/cloud-init)
+ cloudinit/config/modules.py:287:48: error[unresolved-attribute] Type `ModuleType` has no attribute `handle`
+ cloudinit/config/modules.py:298:39: error[unresolved-attribute] Type `ModuleType` has no attribute `handle`
- Found 614 diagnostics
+ Found 616 diagnostics

The NamedTuple here is annotated as having a module: ModuleType field. ModuleType has a __getattr__ method in typeshed that's meant to make code like this easier to write, but we currently always ignore that __getattr__ method when looking up attributes on ModuleType, due to a pre-existing bug.

pywin32

+ win32/Demos/win32gui_menu.py:354:16: error[unsupported-operator] Operator `&` is unsupported between objects of type `int | None` and `Literal[8]`

This looks correct given the definition of UnpackMENUITEMINFO and _MENUITEMINFO in typeshed's stubs for pywin32 (which are installed as part of the primer run when checking pywin32 itself!). The pywin32 code is here.

+ win32/test/test_win32guistruct.py:89:43: error[invalid-argument-type] Argument to function `len` is incorrect: Expected `Sized`, found `str | None`

This has the same underlying cause as the first one (code here).

pyscopg, sympy

The code here is quite complex to untangle, but I can't see any obvious evidence of NamedTuple bugs in these either!

@AlexWaygood AlexWaygood force-pushed the alex-brent/namedtuples branch from d2f07ac to 6aaf82b Compare August 12, 2025 17:43
@AlexWaygood AlexWaygood changed the title [ty] Add precise inference for indexing into NamedTuple instances [ty] Add precise inference for indexing, slicing and unpacking NamedTuple instances Aug 12, 2025
@AlexWaygood AlexWaygood marked this pull request as ready for review August 12, 2025 17:47
Copy link
Member

@dcreager dcreager 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!

Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

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

Fantastic!


let pivot_class = ClassBase::try_from_type(db, pivot_class_type).ok_or({
// TODO: having to get a class-literal just to pass it in here is silly.
// `BoundSuperType` should use a different enum rather than reusing `ClassBase`.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not convinced of this comment; I think the "pivot class" of a bound super object really is a "base class" (that is, it is anything that can exist in an MRO), and it's good to represent it with ClassBase.

I would frame the problem here slightly differently: ClassBase enum should represent "a thing that can be in an MRO", but the constructor ClassBase::try_from_type is also emulating the behavior of __mro_entries__, and there are cases for wanting to construct a "thing that can be in an MRO" from a type without emulating __mro_entries__. So perhaps there should be a way to construct a ClassBase that doesn't model __mro_entries__ (and would thus return None if you tried to use NamedTuple as a base type.)

Copy link
Member Author

Choose a reason for hiding this comment

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

I would frame the problem here slightly differently: ClassBase enum should represent "a thing that can be in an MRO", but the constructor ClassBase::try_from_type is also emulating the behavior of __mro_entries__, and there are cases for wanting to construct a "thing that can be in an MRO" from a type without emulating __mro_entries__. So perhaps there should be a way to construct a ClassBase that doesn't model __mro_entries__ (and would thus return None if you tried to use NamedTuple as a base type.)

Hmm, that's an interesting way of thinking about it... although we don't really attempt to fully emulate __mro_entries__ in ClassBase::try_from_type. We only really try to emulate the parts of __mro_entries__ that consist of 1:1 replacements of one object for another. Some stdlib __mro_entries__ methods implement more complex behaviour where e.g. bases are conditionally appended to the end of the bases list -- we emulate that behaviour in

/// Possibly add `Generic` to the resolved bases list.
///
/// This function is called in two cases:
/// - If we encounter a subscripted `Generic` in the original bases list
/// (`Generic[T]` or similar)
/// - If the class has PEP-695 type parameters,
/// `Generic` is [implicitly appended] to the bases list at runtime
///
/// Whether or not `Generic` is added to the bases list depends on:
/// - Whether `Protocol` is present in the original bases list
/// - Whether any of the bases yet to be visited in the original bases list
/// is a generic alias (which would therefore have `Generic` in its MRO)
///
/// This function emulates the behavior of `typing._GenericAlias.__mro_entries__` at
/// <https://github.com/python/cpython/blob/ad42dc1909bdf8ec775b63fb22ed48ff42797a17/Lib/typing.py#L1487-L1500>.
///
/// [implicitly inherits]: https://docs.python.org/3/reference/compound_stmts.html#generic-classes
fn maybe_add_generic<'db>(
resolved_bases: &mut Vec<ClassBase<'db>>,
original_bases: &[Type<'db>],
remaining_bases: &[Type<'db>],
) {
if original_bases.contains(&Type::SpecialForm(SpecialFormType::Protocol)) {
return;
}
if remaining_bases.iter().any(Type::is_generic_alias) {
return;
}
resolved_bases.push(ClassBase::Generic);
}

To me this sort-of goes to show even more that the methods on ClassBase have (in their current state, at least) been designed only with the internals of our MRO implementation in mind, and don't necessarily make sense if you try to reuse the enum for other purposes. One solution to this (the one this TODO comment currently implies) is just to create a separate enum with the same variants as ClassBase, which you can losslessly convert a ClassBase into. Since ClassBase is not a very big enum at all, I don't think this would lead to much code duplication.

I think BoundSuperType pretty clearly shouldn't be using ClassBase::try_from_type here, at the very least: it leads to obvious false negatives like this -- the super() call here fails at runtime, but we don't detect that, because typing.ChainMap is valid as an entry in a class's bases tuple, so the ClassBase::try_from_type call succeeds:

import typing

# revealed: <super: <class 'ChainMap[Unknown, Unknown]'>, Unknown>
reveal_type(super(typing.ChainMap, typing.ChainMap()))

https://play.ty.dev/fa1b725b-8121-4250-8ad4-604301127a91

For now I'll just make the TODO comment more vague and try to clean this up in a followup.

@AlexWaygood AlexWaygood force-pushed the alex-brent/namedtuples branch from bf12cee to 7cdf408 Compare August 13, 2025 14:39
@AlexWaygood AlexWaygood enabled auto-merge (squash) August 13, 2025 15:13
@AlexWaygood AlexWaygood merged commit 9f6146a into main Aug 13, 2025
37 checks passed
@AlexWaygood AlexWaygood deleted the alex-brent/namedtuples branch August 13, 2025 15:19
dcreager added a commit that referenced this pull request Aug 13, 2025
* main:
  [ty] Add precise inference for indexing, slicing and unpacking `NamedTuple` instances (#19560)
  Add rule code to GitLab description (#19896)
  [ty] render docstrings in hover (#19882)
  [ty] simplify return type of place_from_declarations (#19884)
  [ty] Various minor cleanups to tuple internals (#19891)
  [ty] Improve `sys.version_info` special casing (#19894)
dcreager added a commit that referenced this pull request Aug 13, 2025
…aints

* dcreager/inferrable: (65 commits)
  this was right after all
  mark typevars inferrable as we go
  fix tests
  fix inference of constrained typevars
  [ty] Add precise inference for indexing, slicing and unpacking `NamedTuple` instances (#19560)
  Add rule code to GitLab description (#19896)
  [ty] render docstrings in hover (#19882)
  [ty] simplify return type of place_from_declarations (#19884)
  [ty] Various minor cleanups to tuple internals (#19891)
  [ty] Improve `sys.version_info` special casing (#19894)
  Don't cache files with diagnostics (#19869)
  [ty] support recursive type aliases (#19805)
  [ty] Remove unsafe `salsa::Update` implementations in `tuple.rs` (#19880)
  [ty] Function argument inlay hints (#19269)
  [ty] Remove Salsa interning for `TypedDictType` (#19879)
  Fix `lint.future-annotations` link (#19876)
  [ty] Reduce memory usage of `TupleSpec` and `TupleType` (#19872)
  [ty] Track heap usage of salsa structs (#19790)
  Update salsa to pull in tracked struct changes (#19843)
  [ty] simplify CycleDetector::visit signature (#19873)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants