Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 116 additions & 12 deletions crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ FINAL_A: Final[int] = 1
FINAL_B: Annotated[Final[int], "the annotation for FINAL_B"] = 1
FINAL_C: Final[Annotated[int, "the annotation for FINAL_C"]] = 1
FINAL_D: "Final[int]" = 1
# Note: Some type checkers do not support a separate declaration and
# assignment for `Final` symbols, but it's possible to support this in
# ty, and is useful for code that declares symbols `Final` inside
# `if TYPE_CHECKING` blocks.
FINAL_F: Final[int]
FINAL_F = 1

Expand Down Expand Up @@ -52,7 +48,7 @@ reveal_type(FINAL_D) # revealed: int
reveal_type(FINAL_F) # revealed: int
```

### `Final` without a type
### Bare `Final` without a type

When a symbol is qualified with `Final` but no type is specified, the type is inferred from the
right-hand side of the assignment. We do not union the inferred type with `Unknown`, because the
Expand Down Expand Up @@ -231,7 +227,96 @@ FINAL_LIST: Final[list[int]] = [1, 2, 3]
FINAL_LIST[0] = 4
```

## Too many arguments
## Overriding in subclasses

When a symbol is qualified with `Final` in a class, it cannot be overridden in subclasses.

```py
from typing import Final

class Base:
FINAL_A: Final[int] = 1
FINAL_B: Final[int] = 1
FINAL_C: Final = 1

class Derived(Base):
# TODO: This should be an error
FINAL_A = 2
# TODO: This should be an error
FINAL_B: Final[int] = 2
# TODO: This should be an error
FINAL_C = 2
```

## Syntax and usage

### Legal syntactical positions

Final may only be used in assignments or variable annotations. Using it in any other position is an
error.

```py
from typing import Final, ClassVar, Annotated

LEGAL_A: Final[int] = 1
LEGAL_B: Final = 1
LEGAL_C: Final[int]
LEGAL_C = 1
LEGAL_D: Final
LEGAL_D = 1

class C:
LEGAL_E: ClassVar[Final[int]] = 1
LEGAL_F: Final[ClassVar[int]] = 1
LEGAL_G: Annotated[Final[ClassVar[int]], "metadata"] = 1

def __init__(self):
self.LEGAL_H: Final[int] = 1
self.LEGAL_I: Final[int]
self.LEGAL_I = 1

# TODO: This should be an error
def f(ILLEGAL: Final[int]) -> None:
pass

# TODO: This should be an error
def f() -> Final[None]: ...

# TODO: This should be an error
class Foo(Final[tuple[int]]): ...

# TODO: Show `Unknown` instead of `@Todo` type in the MRO; or ignore `Final` and show the MRO as if `Final` was not there
# revealed: tuple[<class 'Foo'>, @Todo(Inference of subscript on special form), <class 'object'>]
reveal_type(Foo.__mro__)
```

### Attribute assignment outside `__init__`

Qualifying an instance attribute with `Final` outside of `__init__` is not allowed. The instance
attribute must be assigned only once, when the instance is created.
Comment on lines +295 to +296
Copy link
Member

Choose a reason for hiding this comment

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

how does __new__ play into things here? (I can't remember what the spec says.) E.g. this feels ~fine to me... but if the spec disallows it, then the spec disallows it, I guess:

from typing import Final

class Foo:
    def __new__(cls):
        self = object.__new__(cls)
        self.x: Final = 42
        return self

it's basically the pattern that fractions.Fraction uses in the stdlib: https://github.com/python/cpython/blob/9a21df7c0a494e2819775eabd522ebec994d96c0/Lib/fractions.py#L205-L311

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The spec says (emphasis mine):

Finally, as self.id: Final = 1 (also optionally with a type in square brackets). This is allowed only in __init__ methods, so that the final instance attribute is assigned only once when an instance is created.

In your code snippet in particular, any declaration would be illegal in that position. You can't annotate attribute assignments if they don't refer to the first parameter in a method (ty does not yet enforce this, see astral-sh/ty#509).

Copy link
Member

@AlexWaygood AlexWaygood Jul 22, 2025

Choose a reason for hiding this comment

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

hmm, I think there might be value in special-casing __new__ methods specifically so that we allow attribute annotations in __new__ methods where the value of the attribute expression is an instance of cls. But we can defer that discussion to a later PR


```py
from typing import Final

class C:
def some_method(self):
# TODO: This should be an error
self.x: Final[int] = 1
```

### `Final` in loops

Using `Final` in a loop is not allowed.

```py
from typing import Final

for _ in range(10):
# TODO: This should be an error
i: Final[int] = 1
```

### Too many arguments

```py
from typing import Final
Expand All @@ -241,39 +326,58 @@ class C:
x: Final[int, str] = 1
```

## Illegal `Final` in type expression
### Illegal `Final` in type expression

```py
from typing import Final

# error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)"
x: list[Final[int]] = [] # Error!

class C:
# error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)"
# error: [invalid-type-form]
x: Final | int

# error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)"
# error: [invalid-type-form]
y: int | Final[str]
```

## No assignment

Some type checkers do not support a separate declaration and assignment for `Final` symbols, but
it's possible to support this in ty, and is useful for code that declares symbols `Final` inside
`if TYPE_CHECKING` blocks.

### Basic

```py
from typing import Final

DECLARED_THEN_BOUND: Final[int]
DECLARED_THEN_BOUND = 1
```

## No assignment for bare `Final`
### No assignment

```py
from typing import Final

# TODO: This should be an error
NO_RHS: Final
NO_ASSIGNMENT_A: Final
# TODO: This should be an error
NO_ASSIGNMENT_B: Final[int]

class C:
# TODO: This should be an error
NO_RHS: Final
NO_ASSIGNMENT_A: Final
# TODO: This should be an error
NO_ASSIGNMENT_B: Final[int]

# This is okay. `DEFINED_IN_INIT` is defined in `__init__`.
DEFINED_IN_INIT: Final[int]

def __init__(self):
self.DEFINED_IN_INIT = 1
```

## Full diagnostics
Expand Down
Loading