Skip to content

[ty] Allow Final variable assignments in __post_init__#24529

Merged
charliermarsh merged 6 commits intomainfrom
charlie/post-init
Apr 10, 2026
Merged

[ty] Allow Final variable assignments in __post_init__#24529
charliermarsh merged 6 commits intomainfrom
charlie/post-init

Conversation

@charliermarsh
Copy link
Copy Markdown
Member

Summary

As long as the __post_init__ is in a dataclass, e.g., the following is accepted:

from dataclasses import dataclass
from typing import Final

@dataclass
class Test:
    def __post_init__(self):
        self.test_int: Final[int] = 0

Closes astral-sh/ty#3247.

@astral-sh-bot astral-sh-bot Bot added the ty Multi-file analysis & type inference label Apr 10, 2026
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented Apr 10, 2026

Typing conformance results

The percentage of diagnostics emitted that were expected errors held steady at 87.72%. The percentage of expected errors that received a diagnostic held steady at 82.85%. The number of fully passing files held steady at 74/132.

Summary

How are test cases classified?

Each test case represents one expected error annotation or a group of annotations sharing a tag. Counts are per test case, not per diagnostic — multiple diagnostics on the same line count as one. Required annotations (E) are true positives when ty flags the expected location and false negatives when it does not. Optional annotations (E?) are true positives when flagged but true negatives (not false negatives) when not. Tagged annotations (E[tag]) require ty to flag exactly one of the tagged lines; tagged multi-annotations (E[tag+]) allow any number up to the tag count. Flagging unexpected locations counts as a false positive.

Metric Old New Diff Outcome
True Positives 879 879 +0
False Positives 123 123 +0
False Negatives 182 182 +0
Total Diagnostics 1052 1052 +0
Precision 87.72% 87.72% +0.00%
Recall 82.85% 82.85% +0.00%
Passing Files 74/132 74/132 +0

True positives changed (5)

5 diagnostics
Test case Diff

dataclasses_final.py:27

-error[invalid-assignment] Cannot assign to final attribute `final_classvar` on type `<class 'D'>`: `Final` attributes can only be assigned in the class body or `__init__`
+error[invalid-assignment] Cannot assign to final attribute `final_classvar` on type `<class 'D'>`: `Final` attributes can only be assigned in the class body, `__init__`, or `__post_init__` on dataclass-like classes

dataclasses_final.py:35

-error[invalid-assignment] Cannot assign to final attribute `final_no_default` on type `D`: `Final` attributes can only be assigned in the class body or `__init__`
+error[invalid-assignment] Cannot assign to final attribute `final_no_default` on type `D`: `Final` attributes can only be assigned in the class body, `__init__`, or `__post_init__` on dataclass-like classes

dataclasses_final.py:36

-error[invalid-assignment] Cannot assign to final attribute `final_with_default` on type `D`: `Final` attributes can only be assigned in the class body or `__init__`
+error[invalid-assignment] Cannot assign to final attribute `final_with_default` on type `D`: `Final` attributes can only be assigned in the class body, `__init__`, or `__post_init__` on dataclass-like classes

dataclasses_final.py:37

-error[invalid-assignment] Cannot assign to final attribute `final_no_default` on type `<class 'D'>`: `Final` attributes can only be assigned in the class body or `__init__`
+error[invalid-assignment] Cannot assign to final attribute `final_no_default` on type `<class 'D'>`: `Final` attributes can only be assigned in the class body, `__init__`, or `__post_init__` on dataclass-like classes

dataclasses_final.py:38

-error[invalid-assignment] Cannot assign to final attribute `final_with_default` on type `<class 'D'>`: `Final` attributes can only be assigned in the class body or `__init__`
+error[invalid-assignment] Cannot assign to final attribute `final_with_default` on type `<class 'D'>`: `Final` attributes can only be assigned in the class body, `__init__`, or `__post_init__` on dataclass-like classes

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented Apr 10, 2026

Memory usage report

Memory usage unchanged ✅

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented Apr 10, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-assignment 0 1 0
Total 0 1 0

Raw diff:

meson (https://github.com/mesonbuild/meson)
- mesonbuild/options.py:330:9 error[invalid-assignment] Cannot assign to final attribute `default` on type `Self@__post_init__`: `Final` attributes can only be assigned in the class body or `__init__`

Full report with detailed diff (timing results)

@charliermarsh charliermarsh marked this pull request as ready for review April 10, 2026 02:43
Copy link
Copy Markdown
Contributor

@sharkdp sharkdp left a comment

Choose a reason for hiding this comment

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

Thanks!

Comment thread crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md Outdated
@dataclass
class C:
def __post_init__(self):
self.x: Final[int] = 1
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.

Can we add another test here where a field is (accidentally) redeclared? Because I think this should probably be an error? (the developer might think that the field is now final, when it's not)

@dataclass
class C:
    an_actual_field: str

    def __post_init__(self):
        self.an_actual_field: Final[str] = "bar"

If that's hard to do, it may be fine to leave it as a future-work TODO.

Comment on lines +21 to +25
object_ty
.nominal_class(db)
.or_else(|| object_ty.to_class_type(db))
.and_then(|class_ty| class_ty.static_class_literal(db))
.is_some_and(|(class_literal, _)| class_literal.is_dataclass_like(db))
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.

What kind of case is covered by .or_else(|| object_ty.to_class_type(db))? Isn't that conflating instance types with class-literal types somehow? I think I would have expected something like

Suggested change
object_ty
.nominal_class(db)
.or_else(|| object_ty.to_class_type(db))
.and_then(|class_ty| class_ty.static_class_literal(db))
.is_some_and(|(class_literal, _)| class_literal.is_dataclass_like(db))
object_ty.nominal_class(db).is_some_and(|cls| {
cls.static_class_literal(db)
.is_some_and(|(class_literal, _)| class_literal.is_dataclass_like(db))
})

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.

Ah ok, after changing this, it looks like this was necessary to catch these kinds of cases in the conformance suite (just for the diagnostic message in this case): https://github.com/python/typing/blob/1df1565c69730d88ce6877009d268ba1d602af1e/conformance/tests/dataclasses_final.py#L27

Comment on lines +158 to +162
diagnostic.set_primary_message(if allows_post_init {
"`Final` attributes can only be assigned in the class body, `__init__`, or `__post_init__` on dataclass-like classes"
} else {
"`Final` attributes can only be assigned in the class body or `__init__`"
});
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.

👍

@carljm
Copy link
Copy Markdown
Contributor

carljm commented Apr 10, 2026

Haven't reviewed the PR and don't know if it makes anything easier here, but FWIW I don't think we need to restrict this to dataclass-likes. __post_init__ is a dunder, so it's invalid for anyone to use that name for a purpose other than what CPython uses it for -- which is for dataclass-likes. That is to say, if anyone used that method name for any other purpose, and complained that we allow Final in it, it would be their bug not ours.

@sharkdp
Copy link
Copy Markdown
Contributor

sharkdp commented Apr 10, 2026

Haven't reviewed the PR and don't know if it makes anything easier here, but FWIW I don't think we need to restrict this to dataclass-likes.

Thanks, yes. I still think it's valuable to distinguish dataclass-likes and normal classes if only for purposes of special-casing the diagnostic message. I'd rather not mention __post_init__ in the diagnostic message for Final violations on "normal" classes.

@AlexWaygood AlexWaygood removed their request for review April 10, 2026 12:20
@charliermarsh charliermarsh merged commit 5da2161 into main Apr 10, 2026
55 checks passed
@charliermarsh charliermarsh deleted the charlie/post-init branch April 10, 2026 12:35
carljm added a commit that referenced this pull request Apr 10, 2026
* main:
  [ty] Fix bad diagnostic range for incorrect implicit `__init_subclass__` calls (#24541)
  [ty] Add a `SupportedPythonVersion` enum (#24412)
  [ty] Ignore unsupported editor-selected Python versions (#24498)
  [ty] Add snapshots for `__init_subclass__` diagnostics (#24539)
  [ty] Minor fix in tests (#24538)
  [ty] Allow `Final` variable assignments in `__post_init__` (#24529)
  [ty] Expand test suite for assignment errors (#24537)
  [ty] Use `map`, not `__map`, as the name of the mapping parameter in `TypedDict` `__init__` methods (#24535)
  [ty] Rework logic for synthesizing `TypedDict` methods (#24534)
  [flake8-bandit] Fix S103 false positives and negatives in mask analysis (#24424)
  [ty] mdtest.py: update dependencies (#24533)
  Rename patterns and arguments source order iterator method (#24532)
  [ty] Omit invalid keyword arguments from `TypedDict` signature (#24522)
  [ty] support super() in metaclass methods (#24483)
  [ty] Synthesize `__init__` for `TypedDict` (#24476)
carljm added a commit that referenced this pull request Apr 10, 2026
* main:
  Bump typing conformance suite commit to latest upstream (#24553)
  [ty] Reject deleting`Final` attributes (#24508)
  [ty] Respect property deleters in attribute deletion checks (#24500)
  [ty] stop unioning Unknown into types of un-annotated attributes (#24531)
  [ty] Fix bad diagnostic range for incorrect implicit `__init_subclass__` calls (#24541)
  [ty] Add a `SupportedPythonVersion` enum (#24412)
  [ty] Ignore unsupported editor-selected Python versions (#24498)
  [ty] Add snapshots for `__init_subclass__` diagnostics (#24539)
  [ty] Minor fix in tests (#24538)
  [ty] Allow `Final` variable assignments in `__post_init__` (#24529)
  [ty] Expand test suite for assignment errors (#24537)
  [ty] Use `map`, not `__map`, as the name of the mapping parameter in `TypedDict` `__init__` methods (#24535)
  [ty] Rework logic for synthesizing `TypedDict` methods (#24534)
  [flake8-bandit] Fix S103 false positives and negatives in mask analysis (#24424)
  [ty] mdtest.py: update dependencies (#24533)
  Rename patterns and arguments source order iterator method (#24532)
  [ty] Omit invalid keyword arguments from `TypedDict` signature (#24522)
  [ty] support super() in metaclass methods (#24483)
  [ty] Synthesize `__init__` for `TypedDict` (#24476)
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.

Allow Final attributes in __post_init__

3 participants