Skip to content

[ty] Ignore and reject annotations on non-name targets#25324

Merged
charliermarsh merged 5 commits into
mainfrom
charlie/reject
May 27, 2026
Merged

[ty] Ignore and reject annotations on non-name targets#25324
charliermarsh merged 5 commits into
mainfrom
charlie/reject

Conversation

@charliermarsh

@charliermarsh charliermarsh commented May 22, 2026

Copy link
Copy Markdown
Member

Summary

Like Mypy and Pyright, we now ignore annotations on non-name (i.e., subscript and attribute) targets, and emit a diagnostic for the invalid annotation -- with the exception of annotated assignments on self and class attributes (i.e., receivers).

Closes astral-sh/ty#509.

@astral-sh-bot astral-sh-bot Bot added the ty Multi-file analysis & type inference label May 22, 2026
@astral-sh-bot

astral-sh-bot Bot commented May 22, 2026

Copy link
Copy Markdown

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 89.70%. The percentage of expected errors that received a diagnostic held steady at 86.99%. The number of fully passing files held steady at 91/134.

@astral-sh-bot

astral-sh-bot Bot commented May 22, 2026

Copy link
Copy Markdown

Memory usage report

Memory usage unchanged ✅

@astral-sh-bot

astral-sh-bot Bot commented May 22, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

Lint rule Added Removed Changed
unused-type-ignore-comment 0 4 0
Total 0 4 0

Raw diff:

pyppeteer (https://github.com/pyppeteer/pyppeteer)
- pyppeteer/connection.py:99:53 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- pyppeteer/connection.py:100:40 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- pyppeteer/connection.py:228:53 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- pyppeteer/connection.py:229:40 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive

Full report with detailed diff (timing results)

def __init__(self) -> None:
this = self
# error: [invalid-type-form]
# error: [unresolved-attribute]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

These cases are a bit weird, but Mypy and Pyright also reject this.

def set_attribute(value: str):
# error: [invalid-type-form]
# error: [unresolved-attribute]
self.x: str = value

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Pyright rejects this, but Mypy accepts it as defining C.x... Maybe we should accept this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pyrefly also supports this. I think it's fine to wait for a user request. It seems easy to work around with a class-level annotation.

@MichaReiser

Copy link
Copy Markdown
Member

@sharkdp will be so happy to finally have a PR to review ;)

@sharkdp sharkdp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you!

self.x: "int" | "str" = 42

d = {}
# error: [invalid-type-form]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It seems slightly wrong to me to use invalid-type-form as the rule code. The "type form" itself is not invalid, it's in an invalid position. On the other hand, the rule description (https://docs.astral.sh/ty/reference/rules/#invalid-type-form) could be interpreted in a way that would include this case.

I think I'm okay with reusing the code, especially since this seems like a mistake that does not come up very frequently.


@classmethod
def initialize(cls):
cls.class_attr: int = 1 # fine

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

to match the description above, can we also include

Suggested change
cls.class_attr: int = 1 # fine
cls.class_attr: str = None. # snapshot: invalid-assignment
cls.class_attr2: int = 1 # fine

def set_attribute(value: str):
# error: [invalid-type-form]
# error: [unresolved-attribute]
self.x: str = value

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pyrefly also supports this. I think it's fine to wait for a user request. It seems easy to work around with a class-level annotation.

Comment thread crates/ty_python_semantic/src/types/infer/builder/final_attribute.rs Outdated
Comment thread crates/ty_python_semantic/src/types/infer/builder.rs Outdated
Comment thread crates/ty_python_semantic/src/types/infer/builder.rs Outdated
@charliermarsh charliermarsh enabled auto-merge (squash) May 27, 2026 13:54
@charliermarsh charliermarsh merged commit 2428fca into main May 27, 2026
58 of 59 checks passed
@charliermarsh charliermarsh deleted the charlie/reject branch May 27, 2026 13:59
anishgirianish pushed a commit to anishgirianish/ruff that referenced this pull request May 28, 2026
## Summary

Like Mypy and Pyright, we now ignore annotations on non-name (i.e.,
subscript and attribute) targets, and emit a diagnostic for the invalid
annotation -- with the exception of annotated assignments on `self` and
class attributes (i.e., receivers).

Closes astral-sh/ty#509.
charliermarsh added a commit that referenced this pull request Jun 3, 2026
)

## Summary

We now emit `invalid-type-form` for an invalid annotation on a non-name
target (#25324)... But it turns out that we still allow that annotation
to participate in downstream operations, since we create an
annotated-assignment definition for it during the indexing pass.

For example:

```python
def accepts_inner(inner: dict[str, int]): ...

def _(x: dict[str, object]):
    x["inner"]: dict[str, float | str] = {"a": 1, "b": "a"}  # error: [invalid-type-form]
    x["inner"]["c"] = 1.0  # error: [invalid-assignment] because we infer from the RHS, not the annotation
    x["inner"] = {"inner": {"a": 1}}
    accepts_inner(**x["inner"])  # ok
```

The rejected annotation should not widen the dictionary assigned to
`x["inner"]`, nor should it constrain the later replacement. (The same
issue applies to rejected attribute annotations, like `obj.inner: T`.)

We now represent each syntactically indexed declaration outcome as
either `Declared(...)` or `Rejected`. Declaration consumers only use
`Declared(...)`.
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.

Annotated assignments of non-name targets are not checked

3 participants