Implementation of swapped marker order

PEP 508 (11 years ago) permits marker expressions with the variable on either side:

python_version >= '3.9'     # variable on LHS — worked before
'3.9' <= python_version     # variable on RHS — broken before this fix
'3.13.*' == python_full_version  # wildcard failed entirely

However, packaging has never supported this. Someone noticed late last year, opened an issue, and now there are no fewer than three open PRs to add this (some clearly assisted or more by AI):

uv also doesn’t fully support this, but it partially does, which I think is why we’ve started getting PRs to fix it now after all these years. (Specifically, uv pip install "tqdm; '3.13' <= python_version" works with uv, but uv pip install "tqdm; '3.14.*' == python_full_version" doesn’t work - it just always installs regardless of the python version.) Though maybe it’s just because someone noticed it, opened an issue, then people are picking it as an easy thing to fix, I don’t know. No one has pointed at a real package trying to use this.

The question is, do we want to allow these reversed expressions, or do we want to tighten the spec (with a PEP) to match current practice (minus a small number of packages that might have only been tested on uv and do have flipped values here). I’m neutral; I feel as a user I’d expect to be able to write it however I want, but as a developer, I like strict rules and I don’t like arbitrary choice. What do people think?

If people are happy with packaging allowing reversed order, we can merge on of the PRs, I think 1126 was the nicest implementation.

  • Make the standard match packaging (no flipped order)
  • Make packaging match the standard (support either order)
0 voters
2 Likes

I’m not sure I fully understand the comment about uv. But it sounds like this would affect uv as well - in which case, I’m not clear why the vote is only about packaging.

1 Like

We need to get Attempt to clarify environment marker evaluation by ncoghlan · Pull Request #1988 · pypa/packaging.python.org · GitHub approved and merged before we attempt to make any further clarifications to the environment marker specification.

However, I checked if the standard really was ambiguous about this and unlike some of the semantic details, the syntax is explicitly spelled out as symmetric in the grammar: Dependency specifiers - Python Packaging User Guide

2 Likes

If we change the standard, then it would affect uv, though I think we could just leave it as undefined behavior, I don’t think we need to tell uv to reject this - they might want to. If we change packaging, then it doesn’t really affect uv, other than probably making the existing bugs with ".*" on the left a little higher priority.

Correct, this is clearly part of the standard, packaging just has never implemented it, so it’s very rare in practice. If we fix it in packaging, that locks us to the current standard; I just want to make sure people are okay with that before we do.

From what you’re saying, if we don’t change the standard, this would affect uv too (at least in the sense that the existing bugs would be confirmed as bugs).

But I don’t think there’s any justification for changing the standard here - “we didn’t implement part of the standard and it seems OK” is a pretty weak argument.

Why wouldn’t people be happy with packaging implementing the standard as currently written? Even if you vote for the other option, that doesn’t necessarily imply that you’re not OK with following the standard.

1 Like

I’d put it more like “it took over 10 years for someone to notice that packaging didn’t implement this, so let’s make sure we do want it” :wink:

Just in case it becomes relevant, I’ll point out that changing the spec will require a PEP (it’s clearly not just a “clarification”).

3 Likes

Good point, updated above.

Notably, a double-sided constraint like '3.9' <= python_version <= '3.12' is not currently allowed.[1] To me, that is the only context in which I’d put '3.9' on the LHS; in other contexts, I think it hurts readability and is apparently uncommon enough that nobody noticed or cared about it for years. (The number of open PRs doesn’t indicate any interest in the issue itself; they all look like AI bot slop to me.)

Personally, I’d prefer to narrow down the standard to match packaging’s implementation; but the overhead of a PEP is probably larger than that of accepting one PR and having a handful of lines of code in packaging that are effectively never used.


  1. If I understand the grammar correctly, that could be written as '3.9' <= python_version and python_version <= '3.12', though. ↩︎

3 Likes

I have to agree with those who consider changing the standard in favor of disallowing the unimplemented part. Who writes “3 < x” anyway? I know it’s known as Yoda notation, but there’s no need for it here and it is confusing to human readers. At least, when I read something like that, I have to reverse it in my head first before I understand the meaning.

8 Likes

The thing is, it is (partially) implemented in uv. Ultimately, someone has to change their code.

Personally, I don’t really care that much but the fact that changing the spec needs a PEP pushes me to -1 for changing the spec. Especially as packaging already has PRs implementing spec-conformant behaviour, so it’s not even as if there’s much work to do to fix the code.

1 Like

Is there much of a performance hit to parsing both ways? I was under the impression that packagings’s version/requirement/specifier parsers were a big factor in the speed of dependency solving.

Oh, I forget there’s also a more pressing reason to decide on this now: we are adding a public API to Marker, since currently packages have to pull out our internals to operate on them (and quite a few packages are doing that). The new API forces the question: should all consumers have to handle Yoda conditions, or should we flip them automatically, or should we just not allow them (status quo). Supporting Yoda conditions might break the current consumers that are using our private API, as well, if they assume an order to the parts. feat: add API for markers as a structured tree by danyeaw · Pull Request #1145 · pypa/packaging · GitHub

2 Likes

When I initially implemented PEP 508 for uv, I tried to support as many variations as possible, both for spec completeness and for supporting as many packages in the ecosystem as possible. I even built a reporter system for broken and misleading markers when encountered in the dependency tree, but that ended up unused. Much of that support is for cases that are possible in the grammar, but not reasonable to write, often more misleading than helpful, some of which we since fixed on the spec side with the last PEP 508 update. The set of features that users need to express all possible platform conditions, and the set that users actually use, they are both much smaller. If I were implementing PEP 508 again today, I’d cut the scope much tighter around what we minimally need to support.

Reasoning about PEP 508 is hard, it e.g. doesn’t engage with the difference between a version specifier such as 3.9 and a version identifier[1] such as 3.9.*.
[2]. You can try to reason about it mathematically and say == is an equality operator, so it is commutative, and "3.12.0" == "3.12.*" iff "3.12.*" == "3.12.0". But equality is also transitive, which implies that "3.12.0" == "3.12.*" and "3.12.0" == "3.*" imply "3.12.*" == "3.*" as well as "3.12.*" == "3.12.1" and "3.12.*" == "3.12.0" imply "3.12.1" == "3.12.0". (And then there’s ~=, which only ever goes in one direction as there’s no =~, so LHS and RHS aren’t universally flippable.) Another way to reason about it is to say that PEP 440 defines clauses in the shape <op> <version-op> that can be evaluated against a <version>, and PEP 508 molds combines into an evaluatable expression <version> <op> <version-op>. Personally, I like that interpretation for limiting complexity. Another option is to say that we should accept "3.12.*" == "3.12.0" because we know how to evaluate it, and frankly it’s not a complex change in uv.

uv does support reversed markers such as '3.9' <= python_version as I saw them as supported in the spec when implementing markers. The reversed markers don’t add any capabilities, but do add complexity, so I’m not particularly attached to them. The biggest problem here is really that uv is very strict about backwards compatibility and removing support for a from of marker expression is a breaking change.

It gets even sillier with cases such as "3.12" > "3.9" and python_version < python_full_version, which you can reasonably argue being allowed in the spec, but which clearly should not occur.

Personally, I don’t really care that much but the fact that changing the spec needs a PEP pushes me to -1 for changing the spec. Especially as packaging already has PRs implementing spec-conformant behaviour, so it’s not even as if there’s much work to do to fix the code.

This problem is about imprecisions in the current spec. Both from the text and spirit of PEP 508, you can reasonably argue for either behavior. Previously, those gaps were filled by whatever packaging and pip did, and i know some cases where we updated the spec to comply with packaging’s (changed) behavior, for better or worse. My goal is to find consensus between packaging and uv how we will implement these parts, which will - again for better or worse - be the de-facto standard of the ecosystem. There are no capabilities that we lose or gain by whatever choice we make here, the only way users could have a bad time if one supports something that the other tool rejects. TBH I’m not terribly worried about this overall, since the intuitive, documented and sufficient ways to write platform markers work equally with both.


  1. In hopes that I’m using these terms in the sense of PEP 440 ↩︎

  2. “The <version_cmp> operators use the version comparison rules of the Version specifier specification when those are defined (that is when both sides have a valid version specifier)” ↩︎

5 Likes

Honestly, I’d be perfectly happy if someone wanted to rationalise and clarify our version handling specs. But it’s a big undertaking, full of potentially breaking changes, and definitely needs a PEP.

My feeling is that it’s not worth doing a PEP just to cover swapped order, and I didn’t expect anyone to want to tackle the bigger issue. But if someone does, then go for it!

Any grammar change that requires an env_var (like python_version) to only occur on the LHS of a comparison operator and a python_str (like "3.12") to only occur on the RHS of a comparison operator would automatically eliminate those silly cases as a nice side effect.

Looking at the grammar more closely, I’m less certain about the in and not in operators:

  • For extras and dependency_groups, PEP 751 explicitly states that these are sets and that set literals are not allowed; thus, those should only be able to appear on the RHS of an in/not in operator, with a python_str on the LHS.
  • In other cases, both orderings are potentially reasonable (e.g. sys_platform in "linux, darwin" or "freebsd" in sys_platform[1])

If we only have those two cases, that should be representable in the grammar without too much trouble. If there are other cases we’d want to constrain, things may get trickier?

As long as any updated standard doesn’t explicitly say that “clients MUST ignore non-compliant markers”, would that be a problem in practice? uv would still comply with the updated standard (even without any changes to uv) and could decide to warn about or remove support for non-compliant markers on their own time scale.


  1. At least, pre-3.14. ↩︎

Yes, because users may then publish wheels using those markers, which breaks non-uv users.

3 Likes

This particular confusion the draft environment marker evaluation clarification does cover:

All marker comparison expressions are expected to compare a named marker field
against a given user supplied constant. … Tools MAY emit an
error if no marker field is referenced in a comparison (that is, both operands
are given as constants).

(It still doesn’t call out the “two named marker fields” case, though, so that could potentially be tweaked further)

As far as the original question about operand reversibility goes:

  • in, not in: definitely not reversible (and the clarification already calls out that these must be “constant op identifier” to have any valid semantic significance)
  • ==, !=: arguably not reversible when there’s a wildcard value on the right hand side, but could be made reversible via a suitable definition
  • ~=: arguably not reversible due to the implied wildcard, but could be made reversible if == is made reversible
  • <, <=, >=, >: Nominally reversible since there aren’t any wildcard shenanigans, but potential subtleties around the handling of pre- and post- release markers
  • ===: trivially reversible

Based on that, I’m actually going to change my vote (from “change packaging” to “reword the specification”), because the specification is more of a semantic mess on this front than I realised when I wrote my first post above. Punting things into “undefined behaviour” territory is a lousy resolution, but this has been sketchy for so long that we’re not really putting anything in that category, we’re just acknowledging that it already ended up there some time ago.

1 Like

Attempt to clarify environment marker evaluation by ncoghlan · Pull Request #1988 · pypa/packaging.python.org · GitHub took a decent amount of effort from @henryiii and I, but the assessment in the previous thread (linked from the PR) was that we were just on the right side of the “editorial updates” line.

I could certainly turn it into a PEP, but given the intent is to not actually change anything[1], I’m not sure what the PEP could say beyond “Spec confusing, therefore, clarify spec”.

(I definitely wouldn’t want to roll this thread into that PR though, as it really should be a spec change that updates the environment marker grammar to enforce the name [symbolic op] constant and constant [in | not in] name constraints at a syntactic level. An editorial update could potentially add clarifications to declare the reversed forms as “syntactically valid, semantically ambiguous”, though)


  1. (Edit: technically, the PR does declare some otherwise syntactically valid things to be semantic nonsense, and hence grants fairly broad permissions to tools to either disallow those things outright, or short circuit their evaluation to avoid pointless complexity. Those are all cases where the spec was previously entirely silent on what tools should do, though, so they’re more a matter of calling out already undefined behaviour with an assortment of MAY and SHOULD statements, rather than adding any new MUST statements) ↩︎

2 Likes