I am proposing that the from __future__ import annotations feature should eventually get deprecated and removed. The hope is that for most code, this will be uneventful in Python 3.14 with the new annotation semantics: they can just remove the future import and their code will work as before. But there will of course be exceptions, and I’d like to give people more tools to make sure that their code won’t run into any issues when the future import is removed.
-X disable_future=annotations: Disable the future on all files, including ones that contain from __future__ import annotations. This will be useful for testing that your codebase is ready for removing the future import in 3.14.
-X enable_future=annotations: Enable the future on all files, regardless of their __future__ imports. Not something I’d recommend for annotations, but it would have been useful for many past futures (see below), and it makes sense to add it for symmetry.
These are explicitly meant to be for testing; we should recommend against turning them on for production workloads.
A possible downside is that if you use these switches, they will apply to all code you use, including third-party libraries that you cannot easily change. While this can help these libraries find issues, the maintainers might prefer to wait longer before dealing with __future__ changes.
Let’s look at past futures to see whether these switches would have been useful:
generator_stop (added in 3.5, enabled by default in 3.7): I would have used -X enable_future=generator_stop to test my codebase in 3.6. The behavior switch here is obscure enough that it didn’t really feel worth adding the future import to every file, but it would have been useful to test that.
unicode_literals (2.6 and 3.0): Possibly useful for testing though explicit u prefixes would have been preferable.
absolute_import (2.5 and 3.0): Similar to unicode_literals, better to use a linter to enforce absolute imports in all code you control.
print_function (2.6 and 3.0): Wouldn’t make sense to turn this on globally; you’d probably just get SyntaxErrors.
division (2.2 and 3.0): I would have used -X enable_future=division to test my Python 2.7 code for compatibility.
with_statement (2.5 and 2.6) and generators (2.2 and 2.3): I think these were future imports because they added new keywords. Using enable_future could have found places where these keywords were used as identifiers, though of course a linter could do that too.
nested_scopes (2.1 and 2.2): Haven’t fully digested the change there as it long predates me, but it sounds like we already raised a warning for code that would change behavior, so probably not very useful.
I don’t know what futures we’ll add in the future (they’ve become rather less common in Python 3 than 2), but I think this list implies there’s a good chance that these switches will also be useful for future __future__ imports.
That’s a good point. I think -X options in general are considered unstable; for example, we removed -X oldparser in 3.10.
I think we should support these options only for as long as we have support for the future import. So when annotations is completely removed (proposed to be around 3.20 or so), it will be an error to use it with -X enable/disable_future. For all past futures (all of which are now enabled by default), -X enable_future will work (but do nothing as the future is always enabled) and -X disable_future will be an error.
For anyone else that had forgotten why annotations is a special case: it’s a future flag that did not, in fact, end up becoming the future behaviour, so making from __future__ import annotations a no-op is a behavioural change.
The main practical issue I see with this idea is that one of the reasons future flags have to be in the source files they relate to is that modifying the import statements then also serves as a cache buster for the bytecode caching.
I suppose you could combine the new interpreter level flags with the --force option in compileall to ensure you’re genuinely compiling everything with the overrides in place.
Letting the pyc files get intentionally out of sync with their source files still feels like a recipe for subsequent debugging pain, though (if you forget to do a regular compileall invocation afterwards to put everything back to normal).
Masking a future also feels like an edge case that’s only applicable to situations like annotations where we change our mind and schedule the future flag for removal instead of having it become the default behaviour.
So is this really a preferable testing alternative to doing a search and replace that inserts # at the start of every from __future__ import annotations line and testing compatibility that way? Bytecode cache invalidation will work normally as the line gets commented or uncommented, and you can test as few or as many files at a time as you like (with no chance of changing the behaviour of third party components).
future imports are scoped to a file already, and it should be up to the maintainers of projects to determine what applies here.
I’ve consistently read claims from those in favor of the new replacement that it should be a low-impact change. If it’s no longer a low-impact change, there may be other issues with what people believe is happening not matching what ends up being implemented. If it’s still a low-impact change, then I think letting people test it at their own pace shouldn’t have a downside with such a long timeline on removing the future.
I have no intention of supporting without this future import until no version of python still needs it. This encourages testing of this by people I won’t support long before I’m going to consider this.
That’s an artifact of sphinx looking at the value of the annotation rather than the original AST or string expression. PEP-695 type aliases do not have this problem, because you create an actual TypeAliasType instance, rather than make a pure assignment, which is completely transparent once you only look at the value, like you would with the old style of type aliases.
You can achieve the same result prior to Python 3.12 by manually wrapping the annotation in a string or using typing_extensions.TypeAliasType. No future import required. So I don’t think this is relevant to the discussion.
To get back on topic. I don’t have a strong opinion on this change, but it does seem useful as a pure testing feature. You could avoid the potential issues outlined by @ncoghlan if you mandated that these options would always bypass the bytecode cache, i.e. no persistent pyc files are generated or read as long as you enable one of these options.
Or alternatively you could tag the pyc files depending on the supplied options, so the interpreter knows it has to recompile.
I think the best solution would be to disable the pyc cache entirely when these new proposed flags are added, which also helps ensure that the new flags are used only for debugging and testing.
In Python 3.14, tools like Sphinx will be able to use annotationlib.Format.SOURCE to get this behavior, no from __future__ import annotations required.