Skip to content

Comments

[ty] Enforce typing.Final#19178

Merged
sharkdp merged 2 commits intomainfrom
david/enforce-final
Jul 8, 2025
Merged

[ty] Enforce typing.Final#19178
sharkdp merged 2 commits intomainfrom
david/enforce-final

Conversation

@sharkdp
Copy link
Contributor

@sharkdp sharkdp commented Jul 7, 2025

Summary

Emit a diagnostic when a Final-qualified symbol is modified. This first iteration only works for name targets. Tests with TODO comments were added for attribute assignments as well.

related ticket: astral-sh/ty#158

Ecosystem impact

Correctly identified modification of a Final symbol (behind a # type: ignore):

- warning[unused-ignore-comment] sphinx/__init__.py:44:56: Unused blanket `type: ignore` directive

And the same here:

- warning[unused-ignore-comment] src/trio/_core/_run.py:128:45: Unused blanket `type: ignore` directive

Test Plan

New Markdown tests

@sharkdp sharkdp added the ty Multi-file analysis & type inference label Jul 7, 2025
@sharkdp sharkdp force-pushed the david/enforce-final branch from 535561f to 07e539f Compare July 7, 2025 14:05
@github-actions
Copy link
Contributor

github-actions bot commented Jul 7, 2025

mypy_primer results

Changes were detected when running on open source projects
trio (https://github.com/python-trio/trio)
- warning[unused-ignore-comment] src/trio/_core/_run.py:128:45: Unused blanket `type: ignore` directive
- Found 799 diagnostics
+ Found 798 diagnostics

sphinx (https://github.com/sphinx-doc/sphinx)
- warning[unused-ignore-comment] sphinx/__init__.py:44:56: Unused blanket `type: ignore` directive
- Found 606 diagnostics
+ Found 605 diagnostics
Memory usage changes were detected when running on open source projects
flake8 (https://github.com/pycqa/flake8)
-     memo fields = ~63MB
+     memo fields = ~66MB

trio (https://github.com/python-trio/trio)
-     memo fields = ~152MB
+     memo fields = ~159MB

sphinx (https://github.com/sphinx-doc/sphinx)
- TOTAL MEMORY USAGE: ~316MB
+ TOTAL MEMORY USAGE: ~332MB

prefect (https://github.com/PrefectHQ/prefect)
-     memo fields = ~541MB
+     memo fields = ~568MB

@sharkdp sharkdp force-pushed the david/enforce-final branch 2 times, most recently from 3c65586 to 7c267d0 Compare July 8, 2025 09:12
@sharkdp sharkdp force-pushed the david/enforce-final branch from 7c267d0 to 0ecfcc1 Compare July 8, 2025 09:32
@sharkdp sharkdp marked this pull request as ready for review July 8, 2025 11:12
Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

🚀

Comment on lines 157 to 164
from mod import FINAL_A, FINAL_B, FINAL_C, FINAL_D, FINAL_E, FINAL_F

FINAL_A = 2 # error: [invalid-assignment] "Assignment to `Final` symbol is not allowed"
FINAL_B = 2 # error: [invalid-assignment] "Assignment to `Final` symbol is not allowed"
FINAL_C = 2 # error: [invalid-assignment] "Assignment to `Final` symbol is not allowed"
FINAL_D = 2 # error: [invalid-assignment] "Assignment to `Final` symbol is not allowed"
FINAL_E = 2 # error: [invalid-assignment] "Assignment to `Final` symbol is not allowed"
FINAL_F = 2 # error: [invalid-assignment] "Assignment to `Final` symbol is not allowed"
Copy link
Member

Choose a reason for hiding this comment

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

relevant discussion on this: https://discuss.python.org/t/imported-final-variable/82429

I sort-of disagree with the resolution there, but don't have the energy to relitigate the question now 😆 and it comes up very rarely. It's good for us to follow the behaviour the spec mandates, anyway (no change requested!).

Comment on lines +198 to +199
Objects qualified with `Final` *can be modified*. `Final` represents a constant reference to an
object, but that object itself may still be mutable:
Copy link
Member

Choose a reason for hiding this comment

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

Yes -- pyre (and possibly pyrefly? Can't remember the exact status) has a feature called ImmutableRef, which they presented on at the typing summit this year. It's similar to Final but it also prevents any mutations to the symbol. It's a far more invasive and more complicated feature to implement; I'm not sure if it's ever going to be proposed for standardisation in the typing system.

let (declared_ty, is_modifiable) = place_from_declarations(self.db(), declarations)
.and_then(|place_and_quals| {
Ok(
if matches!(place_and_quals.place, Place::Type(_, Boundness::Bound)) {
Copy link
Member

Choose a reason for hiding this comment

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

doesn't need to be done in this PR: should we add a Place::is_definitely_bound() method? It feels like we have a lot of these if matches!() conditions around the codebase

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will look into it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I looked into it, but it seems like such a function would only have this callsite. In most other cases, we also match on the type at the same time.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, fair enough!

Comment on lines 1664 to 1666
if let Some(builder) = self.context.report_lint(&INVALID_ASSIGNMENT, node) {
builder.into_diagnostic("Assignment to `Final` symbol is not allowed");
}
Copy link
Member

Choose a reason for hiding this comment

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

Could we add a subdiagnostic pointing to the previous definition where it was declared as Final?

Copy link
Contributor Author

@sharkdp sharkdp Jul 8, 2025

Choose a reason for hiding this comment

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

Yes, thanks for the suggestion! It's hard to do this for imported symbols (unless @Gankra adds a vector of Definitions to Place 😄), but I added a subdiagnostic for same-module Finals:

image

Comment on lines +32 to +38
3 | MY_CONSTANT: Final[int] = 1
| ----------- Original definition
4 |
5 | # more code
6 |
7 | MY_CONSTANT = 2 # error: [invalid-assignment]
| ^^^^^^^^^^^ Reassignment of `Final` symbol
Copy link
Member

@AlexWaygood AlexWaygood Jul 8, 2025

Choose a reason for hiding this comment

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

even better might be

Suggested change
3 | MY_CONSTANT: Final[int] = 1
| ----------- Original definition
4 |
5 | # more code
6 |
7 | MY_CONSTANT = 2 # error: [invalid-assignment]
| ^^^^^^^^^^^ Reassignment of `Final` symbol
3 | MY_CONSTANT: Final[int] = 1
| ---------- Symbol declared as `Final` here
4 |
5 | # more code
6 |
7 | MY_CONSTANT = 2 # error: [invalid-assignment]
| ^^^^^^^^^^^^^^^ Symbol later reassigned here

but probably not worth spending time on that now if that's hard to achieve!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried to highlight the full range for the assignment, but for (annotated) assignments, full_range apparently only includes the left hand side target. I'll try to change that in a follow-up, since it might affect other diagnostics.

Highlighting the annotation for the original definition would also be nice, but is a bit harder because we would need to explicitly match on different definition kinds.

@sharkdp sharkdp merged commit 149350b into main Jul 8, 2025
37 checks passed
@sharkdp sharkdp deleted the david/enforce-final branch July 8, 2025 14:26
X: Final[int] = 1
def inner():
nonlocal X
# TODO: this should be an error
Copy link
Contributor

Choose a reason for hiding this comment

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

This TODO isn't going to last long: rebasing #19112 on top of this PR makes this line fail :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added that just for you 🎁

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ecosystem-analyzer ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants