You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Add autofix that adds from __future__ import annotations only when it would be useful #18502
Several of Ruff's rules will be more aggressive in the diagnostics and autofixes they emit if from __future__ import annotations is enabled in the file:
For this reason, it's often useful to have from __future__ import annotations at the top of your Python file if you're running Ruff. Ruff knows that it can do a lot more with your code in these situations!
Ruff provides a lint rule FA100 that detects situations where UP035 or UP045 would make autofix edits if from __future__ import annotations was present, and complains that you should probably add that import. But this rule has two problems:
It does not have an autofix
It doesn't tackle situations where other rules such as the TC ones might make edits to your code if from __future__ import annotations were present in the module
Ruff provides a way to ensure that from __future__ import annotations is imported in every file in your project: you can use I002 in combination with the required-imports setting. But applying this to a large existing codebase might cause a huge diff with many unnecessary edits in it, which is probably undesirable.
I propose that we:
Add a configuration option to make FA100 more expansive, so that it also triggers in situations where the TC rules might make changes to your code if from __future__ import annotations was present.
I think this should be behind a configuration option, as the TC rules are very opinionated: not all users will want FA100 to also apply to these situations. Alternatively, we could add a new FA* rule that sits alongside FA100 and flags these situations -- this comment argues in favour of doing that instead.
Check to see if there are other rules where Ruff would be more aggressive if from __future__ import annotations were present. Should we have FA100 also flag modules where these other rules would make edits if from __future__ import annotations were present in the file?
Add an autofix to FA100 that auto-adds from __future__ import annotations
Is the reason for adding a new option and not making this the default behavior because we think this behavior shouldn't be the default or is it mainly for backwards compatibility?
I'm asking because I'm wondering what the behavior would be if we recategorize our rules. I feel like, nudging users towards adding the import is probably the right default behavior. Although I'm not sure where to put this rule if we recategorize the rules: Maybe a style rule?
What's a bit awkward about FA100 is that the behavior is only useful when the other rules are enabled too. Should FA100 not flag adding a from __future__ import annotations if e.g. the TC rules are disabled and adding the future import is only useful to a TC rule?
A third option is to have a global option that TC and the few UP rules check so that they would add the future import as part of their fixes.
To make a decision here, I'd find it useful to have a full list of rules that change behavior based on the presence of a future import with a short description what they use it for.
Is the reason for adding a new option and not making this the default behavior because we think this behavior shouldn't be the default or is it mainly for backwards compatibility?
The whole reason why we received a request for improved autofixes here was that users didn't want to use I002 to ensure that the import was added to every module. They found that too noisy as it made lots of unnecessary edits; they wanted something more targeted that would only add the import in situations where it would be useful.
It's pretty subjective whether adding from __future__ import annotations to a particular module is going to be useful or not. The TC rules are very opinionated; I would guess many fewer users have those enabled than the UP rules. If you don't have the TC rules enabled and you only want from __future__ import annotations added where it would be useful, you'll probably be annoyed if FA100 starts adding the import to files on the basis that it would enable different behaviour from the TC rules.
So I don't think this new behaviour should be the default. Since writing this issue up, I've started leaning towards adding a separate FA rule rather than introducing a new configuration option to FA100.
I think your third option could also make a lot of sense. Though it might mean that we'd end up deprecating FA100, since it would essentially become redundant if its behaviour was just incorporated into the UP rules' autofix
Although I'm not sure where to put this rule if we recategorize the rules: Maybe a style rule?
Definitely stylistic IMO. There can be some minor performance and memory gains from adding the import, but it's rarely noticeable. The main reason for adding it is so that you don't have to quote annotations so often in order to use forward references, and so that you can use newer typing features on older Python versions. They're all primarily stylistic concerns.
Do you think there are cases where users have the TC rules enabled but still don't want this behavior? That was the main reason I thought we could leave out the config option (though I added one in my PR): enabling the TC rules kind of serves as a proxy for the config option. I think you also have to have lint.flake8-type-checking.strict = true in many cases, so to opt in to this new behavior you already need to enable both rules and possibly configure strict. Maybe that's just an implementation detail of my current approach, though.
I do like the idea of a global option, it feels kind of awkward adding these FA100 calls everywhere.
Similarly, I didn't really like the idea of another rule because FA100 is already quite limited in scope. It made sense to me to expand its scope rather than add another narrowly-scoped rule, but maybe that's preferable. I guess it gives users the same kind of flexibility as the config option and could be more intuitive. FA100's name also doesn't really make sense when applied to the TC rules, anyway. That definitely supports adding a separate rule.
I believe these all use the __future__ import to backport their changes to earlier Python versions. For example, the PEP 604 | annotations are only allowed on 3.10 but can be used earlier with a __future__ import.
(UP035 is mentioned in the issue body, but I didn't see any FA100 integration unless I overlooked it. I'm guessing @AlexWaygood meant UP007 since it's tied closely to UP045 but I could also just be wrong.)
(UP035 is mentioned in the issue body, but I didn't see any FA100 integration unless I overlooked it. I'm guessing @AlexWaygood meant UP007 since it's tied closely to UP045 but I could also just be wrong.)
runtime-string-union (TC010) -- I think FA100 is one possible fix for this rule, so a bit of a different relationship from the others but still related
I also checked some of the references for methods like SemanticModel::future_annotations_or_stub that were used for the UP007 and UP045 detection but didn't turn up anything else. Do y'all have any other suggestions for search terms?
I think these cases are all a bit different from the other TC rules because they don't "unlock" new lints, but I guess they're still in the spirit of the issue title because the __future__ import would be useful.
I agree that a global configuration option is probably the way to go here, instead of a dedicated rule. With flake8-type-checking the design in the plugin was that you either choose the TC1xx rules if you want/don't mind the future import, and the TC2xx rules if you want to quote annotations instead.
The TC2xx use-case is currently covered by the quote-annotations setting instead of dedicated rules, but there is nothing covering the TC1xx use-case. My only concern with adding a global setting is that you now could choose both settings at once, which doesn't really make sense. So I would personally deprecate quote-annotations and make the new setting something that lets you make that same choice:
Do nothing (Neither TC1xx nor TC2xx are enabled)
Add a future import when it would help(TC1xx)
Quote type expressions in annotations when it would help (TC2xx)
Currently this would only target annotations.
However the TC2xx behavior arguably can be useful for other type expressions, like casts (uncontroversial) and annotated type aliases (somewhat controversial with runtime typing, more so than the annotations case).
Then there are type expressions that are always deferred, like type statement values and PEP 695 type parameter bounds and PEP 696 type parameter bounds, but they could still technically be relevant for runtime typing. The type statement is once again the controversial case here. Introspection of type parameter bounds and defaults is probably far less common and useful. But the main point here is that while quoting doesn't make sense, you may still choose to make transformations that would not be safe if the value is used at runtime.
So I think that needs some kind of setting as well, determining how aggressive those rules are allowed to be with their transformations. See #14787 for a TC-specific setting for quoting annotations. But it would probably harmonize better with the rest of the rules if it was a shared setting and didn't explicitly talk about quoting or adding future imports.
The most awkward part of these settings is probably the fact that we can't have the same solution for every kind of type expression. You can always quote type expressions, but it doesn't really make sense in certain contexts and adding a future import only helps with annotations pre-Python 3.14. Do we need a separate strategy setting for each kind of type expression or can we get away with something more abstract like proposed in #14787
The TC2xx use-case is currently covered by the quote-annotations setting instead of dedicated rules, but there is nothing covering the TC1xx use-case. My only concern with adding a global setting is that you now could choose both settings at once, which doesn't really make sense. So I would personally deprecate quote-annotations and make the new setting something that lets you make that same choice:
I don't think we have to deprecate the quote-annotations settings or that it would be incompatible. The option is already ignored if a future annotations import is present. We could simply extend it to also disregard the option if this new option is true.
Note that this setting has no effect when from future import annotations is present, as future annotations are always treated equivalently to quoted annotations.
However, I do think unifying the settings is an option for as long as we can come up with a reasonable name. What I find challenging about this is accounting for similar from future import XXX.
So I think that needs some kind of setting as well, determining how aggressive those rules are allowed to be with their transformations. See #14787 for a TC-specific setting for quoting annotations. But it would probably harmonize better with the rest of the rules if it was a shared setting and didn't explicitly talk about quoting or adding future imports.
I think it's fine to have more specific settings where the global setting expresses the general indent: I want to add imports where necessary vs. keep the changes minimal.
The most awkward part of these settings is probably the fact that we can't have the same solution for every kind of type expression. You can always quote type expressions, but it doesn't really make sense in certain contexts and adding a future import only helps with annotations pre-Python 3.14. Do we need a separate strategy setting for each kind of type expression or can we get away with something more abstract like proposed in #14787
Yeah, this is an interesting one and maybe a reason for not unifying the options.
I don't think we have to deprecate the quote-annotations settings or that it would be incompatible. The option is already ignored if a future annotations import is present. We could simply extend it to also disregard the option if this new option is true.
True, although it seems fragile for us having to remember that these two settings can interact badly, unless we prioritize one over the other, so deprecation and combining the settings seems more robust. I can't think of any case where you would want them to both be on in the same module, so it almost certainly would be an erroneous configuration.
It sounds like there's some general consensus around a global setting that modifies the fixes of individual rules to include adding a __future__ import. I think that makes sense, with the caveats that it might mean deprecating FA100 in the future and that it might require some further changes to harmonize with the quoting options. These seem like important considerations, but I feel like we can still move forward here and see how it goes.
If that sounds good to everyone, I'll start working on a new prototype. I'm thinking of the straightforward allow_importing_future_annotations for the option name, but I'm certainly open to suggestions.
I have the aforementioned prototype up in #19100 if any of y'all want to have a look! The logic for emitting the import still feels a little touchy, but I added the config option and a good number of tests, so hopefully it's a decent start for iterating.
I've just played around a bit with ruff on python 3.14 ...
This works as I would expect: TC rules combined with the ruff.lint.future-annotations = true setting do not add a from __future__ import annotations in 3.14, whereas they do in 3.13.
So is it not then the case that from __future__ import annotations should be eagerly removed when running on 3.14 as well? Or am I perhaps missing some other rule / setting which would do auto removal of from __future__ import annotations in Python 3.14? I also have pyupgrade rules enabled.
@MichaReiser Since from __future__ import annotations still changes how annotations are represented at compile time in 3.14 it might not be a good idea yet to remove it by default, there may some corner-cases where code that runs with from __future__ import annotations will not run without it.
While the goal certainly should be to change the offending annotations in such a way, that they work without the future import, it might make more sense to make this either configurable or its own more opinionated rule, since it will cause some amount of manual churn.
Yes, I agree with @Daverball that from __future__ import annotations can't necessarily be removed automatically on Python 3.14. The 3.14 default behaviour is deliberately different from from __future__ import annotations behaviour in many respects. The hope is that most code that only needs to support Python 3.14+ will be able to drop the import without any issue, but it's not the case that the previously opt-in semantics have now become the default -- it's quite different in that respect to from __future__ import print_function or from __future__ import with_statement.
TL;DR -- it sounds like a good idea to have a rule to automatically remove from __future__ import annotations imports for code that only targets Python 3.14+, but I don't think that should be done by default for all code that enables UP010. We should either add a separate rule just for flagging from __future__ import annotations, or add a config option to UP010 that lets users opt into autoremoval of from __future__ import annotations.
Reacted by Micha Reiser, David Salvisberg and Brent Westbrook
Activity
[`flake8-future-annotations`] Add autofix (`FA100`)
MichaReiser commentedon Jun 25, 2025
Is the reason for adding a new option and not making this the default behavior because we think this behavior shouldn't be the default or is it mainly for backwards compatibility?
I'm asking because I'm wondering what the behavior would be if we recategorize our rules. I feel like, nudging users towards adding the import is probably the right default behavior. Although I'm not sure where to put this rule if we recategorize the rules: Maybe a style rule?
MichaReiser commentedon Jun 25, 2025
What's a bit awkward about
FA100is that the behavior is only useful when the other rules are enabled too. ShouldFA100not flag adding afrom __future__ import annotationsif e.g. theTCrules are disabled and adding the future import is only useful to aTCrule?A third option is to have a global option that
TCand the fewUPrules check so that they would add the future import as part of their fixes.To make a decision here, I'd find it useful to have a full list of rules that change behavior based on the presence of a future import with a short description what they use it for.
AlexWaygood commentedon Jun 25, 2025
The whole reason why we received a request for improved autofixes here was that users didn't want to use
I002to ensure that the import was added to every module. They found that too noisy as it made lots of unnecessary edits; they wanted something more targeted that would only add the import in situations where it would be useful.It's pretty subjective whether adding
from __future__ import annotationsto a particular module is going to be useful or not. TheTCrules are very opinionated; I would guess many fewer users have those enabled than theUPrules. If you don't have theTCrules enabled and you only wantfrom __future__ import annotationsadded where it would be useful, you'll probably be annoyed ifFA100starts adding the import to files on the basis that it would enable different behaviour from theTCrules.So I don't think this new behaviour should be the default. Since writing this issue up, I've started leaning towards adding a separate
FArule rather than introducing a new configuration option toFA100.I think your third option could also make a lot of sense. Though it might mean that we'd end up deprecating
FA100, since it would essentially become redundant if its behaviour was just incorporated into theUPrules' autofixAlexWaygood commentedon Jun 25, 2025
Definitely stylistic IMO. There can be some minor performance and memory gains from adding the import, but it's rarely noticeable. The main reason for adding it is so that you don't have to quote annotations so often in order to use forward references, and so that you can use newer typing features on older Python versions. They're all primarily stylistic concerns.
ntBre commentedon Jun 25, 2025
Do you think there are cases where users have the TC rules enabled but still don't want this behavior? That was the main reason I thought we could leave out the config option (though I added one in my PR): enabling the TC rules kind of serves as a proxy for the config option. I think you also have to have
lint.flake8-type-checking.strict = truein many cases, so to opt in to this new behavior you already need to enable both rules and possibly configurestrict. Maybe that's just an implementation detail of my current approach, though.I do like the idea of a global option, it feels kind of awkward adding these FA100 calls everywhere.
Similarly, I didn't really like the idea of another rule because FA100 is already quite limited in scope. It made sense to me to expand its scope rather than add another narrowly-scoped rule, but maybe that's preferable. I guess it gives users the same kind of flexibility as the config option and could be more intuitive. FA100's name also doesn't really make sense when applied to the TC rules, anyway. That definitely supports adding a separate rule.
The rules that currently use FA100 are:
I believe these all use the
__future__import to backport their changes to earlier Python versions. For example, the PEP 604|annotations are only allowed on 3.10 but can be used earlier with a__future__import.(UP035 is mentioned in the issue body, but I didn't see any FA100 integration unless I overlooked it. I'm guessing @AlexWaygood meant UP007 since it's tied closely to UP045 but I could also just be wrong.)
I can work on a list of potential new uses today.
[`flake8-future-annotations`] Add autofix (`FA100`) (#18903)
AlexWaygood commentedon Jun 25, 2025
UP035 only suggests switching from
typing.Dicttodicthere if thetarget-versionis set to Python 3.9+ orfrom __future__ import annotationsis present: https://play.ruff.rs/e7718e04-9bbd-4156-bc71-103634b01f36.And if you're on Python <=3.8 and you don't have
from __future__ import annotationsthere,FA100tells you to add it: https://play.ruff.rs/4ee281fb-34b8-4f99-adf9-6633ea76ee69ntBre commentedon Jun 25, 2025
I'm seeing an UP006 diagnostic in your first playground link, not deprecated-import (UP035).
AlexWaygood commentedon Jun 25, 2025
oops, good point :-)
ntBre commentedon Jun 25, 2025
Other rules I found by grepping for
__future__that could be affected:__future__importI also checked some of the references for methods like
SemanticModel::future_annotations_or_stubthat were used for the UP007 and UP045 detection but didn't turn up anything else. Do y'all have any other suggestions for search terms?I think these cases are all a bit different from the other TC rules because they don't "unlock" new lints, but I guess they're still in the spirit of the issue title because the
__future__import would be useful.Daverball commentedon Jun 26, 2025
I agree that a global configuration option is probably the way to go here, instead of a dedicated rule. With
flake8-type-checkingthe design in the plugin was that you either choose the TC1xx rules if you want/don't mind the future import, and the TC2xx rules if you want to quote annotations instead.The TC2xx use-case is currently covered by the
quote-annotationssetting instead of dedicated rules, but there is nothing covering the TC1xx use-case. My only concern with adding a global setting is that you now could choose both settings at once, which doesn't really make sense. So I would personally deprecatequote-annotationsand make the new setting something that lets you make that same choice:Currently this would only target annotations.
However the TC2xx behavior arguably can be useful for other type expressions, like casts (uncontroversial) and annotated type aliases (somewhat controversial with runtime typing, more so than the annotations case).
Then there are type expressions that are always deferred, like type statement values and PEP 695 type parameter bounds and PEP 696 type parameter bounds, but they could still technically be relevant for runtime typing. The type statement is once again the controversial case here. Introspection of type parameter bounds and defaults is probably far less common and useful. But the main point here is that while quoting doesn't make sense, you may still choose to make transformations that would not be safe if the value is used at runtime.
So I think that needs some kind of setting as well, determining how aggressive those rules are allowed to be with their transformations. See #14787 for a TC-specific setting for quoting annotations. But it would probably harmonize better with the rest of the rules if it was a shared setting and didn't explicitly talk about quoting or adding future imports.
The most awkward part of these settings is probably the fact that we can't have the same solution for every kind of type expression. You can always quote type expressions, but it doesn't really make sense in certain contexts and adding a future import only helps with annotations pre-Python 3.14. Do we need a separate strategy setting for each kind of type expression or can we get away with something more abstract like proposed in #14787
MichaReiser commentedon Jun 26, 2025
I don't think we have to deprecate the
quote-annotationssettings or that it would be incompatible. The option is already ignored if a future annotations import is present. We could simply extend it to also disregard the option if this new option is true.However, I do think unifying the settings is an option for as long as we can come up with a reasonable name. What I find challenging about this is accounting for similar
from future import XXX.I think it's fine to have more specific settings where the global setting expresses the general indent: I want to add imports where necessary vs. keep the changes minimal.
Yeah, this is an interesting one and maybe a reason for not unifying the options.
Daverball commentedon Jun 27, 2025
True, although it seems fragile for us having to remember that these two settings can interact badly, unless we prioritize one over the other, so deprecation and combining the settings seems more robust. I can't think of any case where you would want them to both be on in the same module, so it almost certainly would be an erroneous configuration.
ntBre commentedon Jun 27, 2025
It sounds like there's some general consensus around a global setting that modifies the fixes of individual rules to include adding a
__future__import. I think that makes sense, with the caveats that it might mean deprecating FA100 in the future and that it might require some further changes to harmonize with the quoting options. These seem like important considerations, but I feel like we can still move forward here and see how it goes.If that sounds good to everyone, I'll start working on a new prototype. I'm thinking of the straightforward
allow_importing_future_annotationsfor the option name, but I'm certainly open to suggestions.ntBre commentedon Jul 3, 2025
I have the aforementioned prototype up in #19100 if any of y'all want to have a look! The logic for emitting the import still feels a little touchy, but I added the config option and a good number of tests, so hopefully it's a decent start for iterating.
[`flake8-type-checking`, `pyupgrade`, `ruff`] Add `from __future__ im…
ollie-bell commentedon Sep 30, 2025
I've just played around a bit with ruff on python 3.14 ...
This works as I would expect:
TCrules combined with theruff.lint.future-annotations = truesetting do not add afrom __future__ import annotationsin 3.14, whereas they do in 3.13.So is it not then the case that
from __future__ import annotationsshould be eagerly removed when running on 3.14 as well? Or am I perhaps missing some other rule / setting which would do auto removal offrom __future__ import annotationsin Python 3.14? I also have pyupgrade rules enabled.MichaReiser commentedon Oct 1, 2025
@ollie-bell Removing unnecessary imports should be UP010. @dylwil3 did we update the rule to mark all annotation imports as unused in Python 3.14?
Daverball commentedon Oct 1, 2025
@MichaReiser Since
from __future__ import annotationsstill changes how annotations are represented at compile time in 3.14 it might not be a good idea yet to remove it by default, there may some corner-cases where code that runs withfrom __future__ import annotationswill not run without it.While the goal certainly should be to change the offending annotations in such a way, that they work without the future import, it might make more sense to make this either configurable or its own more opinionated rule, since it will cause some amount of manual churn.
AlexWaygood commentedon Oct 1, 2025
Yes, I agree with @Daverball that
from __future__ import annotationscan't necessarily be removed automatically on Python 3.14. The 3.14 default behaviour is deliberately different fromfrom __future__ import annotationsbehaviour in many respects. The hope is that most code that only needs to support Python 3.14+ will be able to drop the import without any issue, but it's not the case that the previously opt-in semantics have now become the default -- it's quite different in that respect tofrom __future__ import print_functionorfrom __future__ import with_statement.TL;DR -- it sounds like a good idea to have a rule to automatically remove
from __future__ import annotationsimports for code that only targets Python 3.14+, but I don't think that should be done by default for all code that enablesUP010. We should either add a separate rule just for flaggingfrom __future__ import annotations, or add a config option toUP010that lets users opt into autoremoval offrom __future__ import annotations.ollie-bell commentedon Oct 1, 2025
Yes FWIW i was referring to the case when you have
requires-python >= 3.14in the context of eager removal offrom __future__ import annotationsfrom __future__ import annotationsin more rules #19359[`flake8-future-annotations`] Add autofix (`FA100`) (#18903)
[`flake8-type-checking`, `pyupgrade`, `ruff`] Add `from __future__ im…
[`flake8-future-annotations`] Add autofix (`FA100`) (#18903)