[ty] implement typing.NewType as Type::NewTypeInstance#21157
Conversation
Diagnostic diff on typing conformance testsChanges were detected when running ty on typing conformance tests--- old-output.txt 2025-11-10 22:18:44.267868777 +0000
+++ new-output.txt 2025-11-10 22:18:47.672897783 +0000
@@ -1,4 +1,4 @@
-fatal[panic] Panicked at /home/runner/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/05a9af7/src/function/execute.rs:451:17 when checking `/home/runner/work/ruff/ruff/typing/conformance/tests/aliases_typealiastype.py`: `infer_definition_types(Id(18f71)): execute: too many cycle iterations`
+fatal[panic] Panicked at /home/runner/.cargo/git/checkouts/salsa-e6f3bb7c2a062968/05a9af7/src/function/execute.rs:451:17 when checking `/home/runner/work/ruff/ruff/typing/conformance/tests/aliases_typealiastype.py`: `infer_definition_types(Id(19371)): 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__`
@@ -38,11 +38,19 @@
aliases_implicit.py:118:10: error[invalid-type-form] Variable of type `Literal["int"]` is not allowed in a type expression
aliases_implicit.py:119:10: error[invalid-type-form] Variable of type `Literal["int | str"]` is not allowed in a type expression
aliases_implicit.py:133:6: error[call-non-callable] Object of type `UnionType` is not callable
-aliases_newtype.py:15:1: error[type-assertion-failure] Argument does not have asserted type `int`
-aliases_newtype.py:18:1: error[invalid-assignment] Object of type `NewType` is not assignable to `type`
-aliases_newtype.py:23:16: error[invalid-argument-type] Argument to function `isinstance` is incorrect: Expected `type | UnionType | tuple[Unknown, ...]`, found `NewType`
-aliases_newtype.py:26:21: error[invalid-base] Invalid class base with type `NewType`
-aliases_newtype.py:63:43: error[too-many-positional-arguments] Too many positional arguments to bound method `__init__`: expected 3, got 4
+aliases_newtype.py:11:8: error[invalid-argument-type] Argument is incorrect: Expected `int`, found `Literal["user"]`
+aliases_newtype.py:12:1: error[invalid-assignment] Object of type `Literal[42]` is not assignable to `UserId`
+aliases_newtype.py:18:1: error[invalid-assignment] Object of type `<NewType pseudo-class 'UserId'>` is not assignable to `type`
+aliases_newtype.py:23:16: error[invalid-argument-type] Argument to function `isinstance` is incorrect: Expected `type | UnionType | tuple[Unknown, ...]`, found `<NewType pseudo-class 'UserId'>`
+aliases_newtype.py:26:21: error[invalid-base] Cannot subclass an instance of NewType
+aliases_newtype.py:35:1: error[invalid-newtype] The name of a `NewType` (`BadName`) must match the name of the variable it is assigned to (`GoodName`)
+aliases_newtype.py:41:6: error[invalid-type-form] `GoodNewType1` is a `NewType` and cannot be specialized
+aliases_newtype.py:47:38: error[invalid-newtype] invalid base for `typing.NewType`: type `int | str`
+aliases_newtype.py:52:38: error[invalid-newtype] invalid base for `typing.NewType`: type `Hashable`
+aliases_newtype.py:54:38: error[invalid-newtype] invalid base for `typing.NewType`: type `Literal[7]`
+aliases_newtype.py:61:38: error[invalid-newtype] invalid base for `typing.NewType`: type `TD1`
+aliases_newtype.py:63:15: error[invalid-newtype] Wrong number of arguments in `NewType` creation, expected 2, found 3
+aliases_newtype.py:65:38: error[invalid-newtype] invalid base for `typing.NewType`: type `Any`
aliases_type_statement.py:10:19: error[too-many-positional-arguments] Too many positional arguments: expected 1, got 3
aliases_type_statement.py:17:1: error[unresolved-attribute] Object of type `typing.TypeAliasType` has no attribute `bit_count`
aliases_type_statement.py:19:1: error[call-non-callable] Object of type `TypeAliasType` is not callable
@@ -579,7 +587,7 @@
generics_typevartuple_basic.py:12:14: error[invalid-argument-type] `@Todo(starred expression)` is not a valid argument to `Generic`
generics_typevartuple_basic.py:16:26: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `tuple[@Todo(PEP 646), ...]`
generics_typevartuple_basic.py:23:13: error[invalid-argument-type] `@Todo(starred expression)` is not a valid argument to `Generic`
-generics_typevartuple_basic.py:42:34: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `tuple[@Todo(PEP 646), ...]`, found `Literal[1]`
+generics_typevartuple_basic.py:42:34: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `tuple[@Todo(PEP 646), ...]`, found `Height`
generics_typevartuple_basic.py:65:27: error[unknown-argument] Argument `covariant` does not match any known parameter of function `__new__`
generics_typevartuple_basic.py:66:27: error[too-many-positional-arguments] Too many positional arguments to function `__new__`: expected 2, got 4
generics_typevartuple_basic.py:67:27: error[unknown-argument] Argument `bound` does not match any known parameter of function `__new__`
@@ -999,5 +1007,5 @@
typeddicts_usage.py:28:17: error[missing-typed-dict-key] Missing required key 'name' in TypedDict `Movie` constructor
typeddicts_usage.py:28:18: error[invalid-key] Invalid key for TypedDict `Movie`: Unknown key "title"
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 1001 diagnostics
+Found 1009 diagnostics
WARN A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details.
|
typing.NewType as Type::NewTypeInstancetyping.NewType as Type::NewTypeInstance
|
| Lint rule | Added | Removed | Changed |
|---|---|---|---|
invalid-argument-type |
498 | 0 | 8 |
unused-ignore-comment |
0 | 48 | 0 |
invalid-type-form |
2 | 22 | 0 |
unsupported-operator |
4 | 0 | 5 |
invalid-newtype |
5 | 0 | 0 |
invalid-assignment |
3 | 0 | 1 |
invalid-return-type |
1 | 1 | 2 |
type-assertion-failure |
2 | 0 | 2 |
no-matching-overload |
3 | 0 | 0 |
call-non-callable |
0 | 0 | 1 |
non-subscriptable |
1 | 0 | 0 |
unresolved-attribute |
0 | 0 | 1 |
| Total | 519 | 71 | 20 |
This comment was marked as resolved.
This comment was marked as resolved.
611cbb3 to
893a2a5
Compare
|
|
Relevant to the error formatting tweak I just pushed in daeb800, do we like the example in that commit message: from typing import NewType
from nonexistent_module import Foo
Bar = NewType("Bar", Foo)That's a result of the check that says a |
Nice, these changes all look like true positives -- places where we should have been emitting diagnostics, but we previously weren't! |
I think it would be good to suppress the diagnostic here if the second argument has a dynamic type. The user will get a diagnostic from the unresolved import in your example; there's not much use in having cascading diagnostics later on in the module due to the same issue. |
AlexWaygood
left a comment
There was a problem hiding this comment.
Here's my full review. Overall this looks excellent!! This definitely looks like right overall approach IMO.
My main feedback is that it would be great to have some more tests for various parts of the behaviour that you're implementing here. I left some comments inline about things that look correct, but that it would be great to explicitly test. It might also be worth looking through the typing conformance-suite tests for newtype, and/or mypy's tests for newtype, to see if there are any other things that would be worth testing for explicitly.
daeb800 to
7d3cec6
Compare
AlexWaygood
left a comment
There was a problem hiding this comment.
Looking really good -- a couple more small comments!
c6b8501 to
94cec7a
Compare
AlexWaygood
left a comment
There was a problem hiding this comment.
Let's go!
Lots of minor comments below, but nothing blocking; feel free to defer stuff to followup PRs. This looks great!
crates/ty_python_semantic/resources/mdtest/annotations/new_types.md
Outdated
Show resolved
Hide resolved
crates/ty_python_semantic/resources/mdtest/annotations/new_types.md
Outdated
Show resolved
Hide resolved
crates/ty_python_semantic/resources/mdtest/annotations/new_types.md
Outdated
Show resolved
Hide resolved
d4bc986 to
afaca47
Compare
crates/ty_python_semantic/resources/mdtest/annotations/new_types.md
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
#21157 (comment) is the only remaining significant thing I think that's missing from our NewType implementation now! And it's absolutely fine to defer that to a followup IMO
b3c8753 to
7bbb864
Compare
|
|
||
| # But a free typevar is not allowed. | ||
| T = TypeVar("T") | ||
| C = NewType("C", list[T]) # TODO: should be "error: [invalid-newtype]" |
There was a problem hiding this comment.
@AlexWaygood do we like to open issues for these, or is the comment itself sufficient?
There was a problem hiding this comment.
This is something mandated by the spec, so it would be great to open an issue for it. I don't see it as particularly high priority right now, though!
There was a problem hiding this comment.
carljm
left a comment
There was a problem hiding this comment.
Whoops I'm slightly slow on the draw here -- just one comment on a comment :) Probably doesn't even matter enough to address in a follow-up PR.
Awesome work here!
There was a problem hiding this comment.
This test suite is fantastic. So thorough, so clear, all the prose is on-point. Thank you!!
There was a problem hiding this comment.
A lot of those cases are copy/pasted from @AlexWaygood's suggestions, to be clear 😅 Obviously all the bits that are like
from ty_extensions import is_bivariant_endofunctor
There was a problem hiding this comment.
No no, bivariant endofunctors are your assignment for the GA release
| ); | ||
| } | ||
|
|
||
| // Inference of `tp` must be deferred, to avoid cycles. |
There was a problem hiding this comment.
Not clear what the name tp refers to here?
| // Inference of `tp` must be deferred, to avoid cycles. | |
| // Inference of the base type must be deferred, to avoid cycles. |
There was a problem hiding this comment.
Ah, the names of NewType's arguments are name and tp. I guess I spent enough time swimming in this that I forgot how non-obvious that is. NewType technically supports keyword args, though I didn't include that in this PR, and given all the "only simple assignments are allowed" behavior here and in other typecheckers, I'm not sure whether it would be a good idea to use them...
There was a problem hiding this comment.
I ended up supporting keyword args for TypeVar. I think it's totally fine to base our decision for NewType on whether we ever get a user report. (For reference, it looks like pyright does support keyword arguments with NewType, but neither mypy nor pyrefly does.)
Fixes astral-sh/ty#742. This PR rewrites and supersedes my previous
NewTypePR, #20126. The high-level changes:Type::NewTypeInstanceinstead of expandingNominalInstanceInner.infer_legacy_typevarinTypeInferenceBuilder::infer_assignment_definition_impl.There are severalI've moved this out of Draft.// REVIEWERS:questions inline, and I expect I've gone about some things the wrong way (particularly cycle detection?), so I'm leaving this in Draft status for now.