Ban negative extras (extra != markers)#2085
Conversation
Negative extras (`extra !=`) are currently technically allowed in the grammar, but they behave strangely: `extra !=` means that package could transitively remove a package from selection through activating an extra. This turns incremental resolution into a global problem: We can't exit a resolution with a conflict when we've exhausted all versions in range including backtracking, we also have to exhaustively search for any package version that may activate the extra which deactivates the `extra !=` requirement which is part of the conflict. This would be fatal to resolver complexity and performance, preventing most of the short-circuiting that makes resolving tractable. Much weirder, with negative extras, adding a new dependency can make a previously impossible resolution solvable: ``` [project] name = "foo" version = "0.1.0" dependencies = [ "numpy >2", "numpy <2; extra != 'old-science'", ] ``` Only by adding a package with `Requires-Dist: foo[old-science]`, this resolves. Using PEP 621, users only get positive extras: `project.optional-dependencies.foo` gets translated into a conjunction of `extra == "foo"` with each requirement's marker. While this may look like a breaking change, it only clarifies the current state of support in tools. From [pip not supporting it](pypa/pip#14139) and [uv not supporting it](astral-sh/uv#20107), we know it is not currently used and safe to remove. This solves the [confusion for users who want to use negative extras for conflicting packages](astral-sh/uv#20107).
| * treat ``>`` and ``<`` as always being False | ||
|
|
||
| For ``Set of String`` fields, as there is no marker syntax for set literals, | ||
| the only valid operations are ``in`` and ``not in`` comparisons with a user |
There was a problem hiding this comment.
Do we also need to amend the "Set of Strings" definition? If extra != "name" doesn't make sense, do "name" not in extras and "name" not in dependency_groups actually make sense?
ncoghlan
left a comment
There was a problem hiding this comment.
The required change before approval is the history entry.
The proposed wording tweak is genuinely just a suggestion, and the "Set of Strings" question could be resolved in a follow-up PR if we're not sure the exact same concern applies.
| work. [#marker_comparison_logic]_ | ||
| - January 2026: fix outdated references to other documents that were | ||
| inadvertently retained from :pep:`508` | ||
|
|
There was a problem hiding this comment.
History entry is needed for the change
| names used for these comparisons is the full set of requested extras for *that | ||
| particular package*, whether requested directly in a top level dependency | ||
| declaration, or indirectly in a transitive dependency declaration. Other | ||
| comparison operations on ``extra`` are not defined and publishing tools SHOULD |
There was a problem hiding this comment.
Perhaps explicitly call out "... other comparison operations on extra (including !=) are not`" so it's clear the omission from the earlier sentence isn't accidental?
|
@dimbleby Thanks for the notification.
If "tools" is only pip and uv, you are probably right. However, I think it is kind of breaking for Poetry, see e.g. https://python-poetry.org/docs/dependency-specification/#exclusive-extras I mean, Poetry can still document that this is not allowed anymore by the standard and should only be used in Poetry-only projects. |
Hi @radoering, I checked Poetry's dependency selection behavior and found it is almost identical to uv's: it evaluates negative extras ( So I would consider that Poetry also doesn't support negative extras in dependency metadata. Where Poetry does honor a negative extra is when it installs the root project itself ( So banning Independently, I'd strongly recommend Poetry document the limitation that the pattern only works for the top-level project: FYI here's an MRE for my findings: https://gist.github.com/notatallshaw/b0ec3db17c7647e3e9e8db7d9b6288a3
My findings indicate that this is effectively the case already, just undocumented. |
|
@notatallshaw Thanks for the analysis. I checked why poetry-core ignores dependencies with negative extras. Actually, that does not seem to be intentionally but is just a side effect of an unrelated bugfix. We are probably lucky now that python-poetry/poetry#10120 has not been fixed yet. Same for ignoring negative extras from wheels.
You are right. Due to the bugs, the impact of banning negative extras is less severe than I thought. It is probably sufficient to update Poetry's documentation and pay attention not to fix the bugs in a way that ignores the ban. |
Ah I see, but even if it was fixed and the marker evaluation honored |
|
From https://discuss.python.org/t/ban-negative-extras-for-extras-marker/108022/18, it sounds like the intended use case for The guidance we give for attempts to use the "Set of Strings" fields in public dependency declarations is:
The actual comparison operations themselves are left defined as normal rather than giving tools permission to treat them as This isn't the effect we get from undefining
Accordingly, I think the end result will be clearer if we add an explicit clause for negative extra markers along the lines of:
(Having that explicit clause there should also help remind us that even if we someday allow |
Negative extras (
extra !=) are currently technically allowed in the grammar, but they behave strangely:extra !=means that package could transitively remove a package from selection through activating an extra. This turns incremental resolution into a global problem: We can't exit a resolution with a conflict when we've exhausted all versions in range including backtracking, we also have to exhaustively search for any package version that may activate the extra which deactivates theextra !=requirement which is part of the conflict. This would be fatal to resolver complexity and performance, preventing most of the short-circuiting that makes resolving tractable.Much weirder, with negative extras, adding a new dependency can make a previously impossible resolution solvable:
Only by adding a package with
Requires-Dist: foo[old-science], this resolves.Using PEP 621, users only get positive extras:
project.optional-dependencies.foogets translated into a conjunction ofextra == "foo"with each requirement's marker.While this may look like a breaking change, it only clarifies the current state of support in tools. From pip not supporting it and uv not supporting it, we know it is not currently used and safe to remove. This solves the confusion for users who want to use negative extras for conflicting packages.
📚 Documentation preview 📚: https://python-packaging-user-guide--2085.org.readthedocs.build/en/2085/