Skip to content

Add autofix that adds from __future__ import annotations only when it would be useful #18502

Open

Description

@AlexWaygood

Summary

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

Activity

  1. added
    ruleImplementing or modifying a lint rule
    fixesRelated to suggested fixes for violations
    on Jun 6, 2025
  2. self-assigned this
    on Jun 24, 2025
  3. MichaReiser commented on Jun 25, 2025

    @MichaReiser
    Member

    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?

  4. MichaReiser commented on Jun 25, 2025

    @MichaReiser
    Member

    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.

  5. AlexWaygood commented on Jun 25, 2025

    @AlexWaygood
    MemberAuthor

    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

  6. AlexWaygood commented on Jun 25, 2025

    @AlexWaygood
    MemberAuthor

    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.

  7. ntBre commented on Jun 25, 2025

    @ntBre
    Contributor

    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.

    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.

  8. AlexWaygood commented on Jun 25, 2025

    @AlexWaygood
    MemberAuthor

    (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 only suggests switching from typing.Dict to dict here if the target-version is set to Python 3.9+ or from __future__ import annotations is 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 annotations there, FA100 tells you to add it: https://play.ruff.rs/4ee281fb-34b8-4f99-adf9-6633ea76ee69

  9. ntBre commented on Jun 25, 2025

    @ntBre
    Contributor

    I'm seeing an UP006 diagnostic in your first playground link, not deprecated-import (UP035).

  10. AlexWaygood commented on Jun 25, 2025

    @AlexWaygood
    MemberAuthor

    I'm seeing an UP006 diagnostic in your first playground link, not deprecated-import (UP035).

    oops, good point :-)

  11. ntBre commented on Jun 25, 2025

    @ntBre
    Contributor

    Other rules I found by grepping for __future__ that could be affected:

    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.

  12. Daverball commented on Jun 26, 2025

    @Daverball
    Contributor

    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

  13. MichaReiser commented on Jun 26, 2025

    @MichaReiser
    Member

    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.

  14. Daverball commented on Jun 27, 2025

    @Daverball
    Contributor

    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.

  15. ntBre commented on Jun 27, 2025

    @ntBre
    Contributor

    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.

  16. ntBre commented on Jul 3, 2025

    @ntBre
    Contributor

    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.

  17. ollie-bell commented on Sep 30, 2025

    @ollie-bell

    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.

  18. MichaReiser commented on Oct 1, 2025

    @MichaReiser
    Member

    @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?

  19. Daverball commented on Oct 1, 2025

    @Daverball
    Contributor

    @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.

  20. AlexWaygood commented on Oct 1, 2025

    @AlexWaygood
    MemberAuthor

    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.

  21. ollie-bell commented on Oct 1, 2025

    @ollie-bell

    Yes FWIW i was referring to the case when you have requires-python >= 3.14 in the context of eager removal of from __future__ import annotations

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

Metadata

Metadata

Assignees

Labels

fixesRelated to suggested fixes for violationsruleImplementing or modifying a lint rule

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions