CEP 33 - Version literals and their ordering
| Title | Version literals and their ordering |
| Status | Accepted |
| Author(s) | Jaime Rodríguez-Guerra <jaime.rogue@gmail.com>, Bas Zalmstra <bas@prefix.dev> |
| Created | Sep 26, 2025 |
| Updated | Mar 4, 2026 |
| Discussion | https://github.com/conda/ceps/pull/132 |
| Implementation | https://github.com/conda/conda/blob/6614653b1d9bdbffcef55e338d3220daed70c7f8/conda/models/version.py#L52, https://github.com/conda/rattler/blob/rattler-v0.37.4/crates/rattler_conda_types/src/version/mod.rs#L141 |
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC2119 when, and only when, they appear in all capitals, as shown here.
Abstract
This CEP describes version literals as used in the conda ecosystem, and their ordering.
Motivation
The motivation of this CEP is mostly informative, but will also try to clarify some ambiguous details that should be homogenized across existing implementations.
Specification
Version literals
CEP 26 only discussed the type of characters that can be part of a version string (or literal), and its maximum length:
[...] version strings MUST only consist of digits, periods, lowercase ASCII letters, underscores, plus symbols, and exclamation marks. The maximum length of a version string MUST NOT exceed 64 characters.
The present CEP extends these rules with additional constraints:
- Version literals MUST be composed of alphanumeric characters
[A-Za-z0-9], separated into segments by periods.and underscores_. Dashes-are historically allowed and interpreted as underscores, but SHOULD NOT be used because they break filename conventions. - Consecutive runs of digits MUST NOT exceed a value of
2^31-1. - Empty segments (i.e. two consecutive periods, or a period plus an underscore) SHOULD NOT be allowed.
- A single trailing underscore MAY be used exceptionally for comparisons against
openssl 1.x-like version schemes (e.g.1.0.1_ < 1.0.1a). - A single epoch number (a positive integer followed by
!) MAY prefix the rest of the string. - A single local version string MAY be added at the end, separated by a plus symbol
+.
Ordering
Before being compared, version literals MUST be parsed into a list of segments (with each segment being a list of components) as follows:
- They are first split into epoch, main version, and local version at
!and+respectively.- If there is no
!, the epoch is set to0. - If there is no
+, the local version is empty.
- If there is no
- The main version part is then split into components at
.,_, and-.- Each component is split again into consecutive runs of numerals and non-numerals.
- Subcomponents containing only numerals are converted to integers.
- Strings are converted to lowercase, with special treatment for
devandpost. - Trailing underscores are considered part of the preceding string, if any.
- When a component starts with a letter, the fill value
0is inserted before the letter. - Leading zeros in a component are removed.
- The epoch and main version segments are concatenated.
- The same is repeated for the local version part, and stored as a separate list of segments.
For example:
>>> parse("1.2g.beta15.rc")
[[0], [1], [2, 'g'], [0, 'beta', 15], [0, 'rc']], []
>>> parse("1!2.15.1_ALPHA")
[[1], [2], [15], [1], [0, 'alpha']], []
>>> parse("1!2.15.1alpha_")
[[1], [2], [15], [1, 'alpha_']], []
>>> parse("1!2.15.1_alpha+1.2.3h123")
[[1], [2], [15], [1], [0, 'alpha']], [[1], [2], [3, 'h', 123]]
The resulting list of components MUST be compared as follows:
- Integers are compared numerically.
- Strings are compared lexicographically, case-insensitive. The substring
devis always smaller. - Strings are considered smaller than integers, except for
post, which is always greater. - When a component has no correspondent, the missing component is assumed to be
0. - Local versions are only compared when the main versions are identical. A version without a local part is treated as having an implicit local version of 0.
Warning: Pre-releases markers are sensitive to leading zeros and periods. While
"1.1.0" == "1.1.0.0" == "1.1", the rule "When a component starts with a letter, the fill value0is inserted" results in"1.1.0rc" == "1.1.rc" > "1.1rc". See conda#12568.
Rationale
- The
devsubstring is handled differently to allowdevpre-releases to sort before alphas, betas, and release candidates. - The
postsubstring is handled differently to allowpostreleases to sort after any equivalent final release. - Missing components are treated like
0to allow equivalences like'1.1' == '1.1.0'. - The
0fill value is used in components starting with letters to keep numbers and strings in phase, resulting in'1.1.a1' == '1.1.0a1'. - Consecutive runs of digits are limited to prevent integer overflow issues upon parsing. The upper bound is the maximum value for 32-bit unsigned integers because MSVC still defaults to that for
int.
Rejected ideas
conda's version ordering is often compared to Python's PEP 440 and following adjustments, but they are not the same specification. We chose not to incorporate many good ideas in that specification so this CEP represents the current state of the ecosystem. In the future, we may revisit some of these rules to accommodate for special cases in prerelease ordering and their synonyms.
Backwards compatibility
This CEP extends CEP 26 with more details about version literals.
It respects existing implementations and does not break backwards compatibility.
Further work
This CEP only standardizes the current behavior exhibited across most implementations. There are many edge cases that the authors would like to improve in future efforts. Examples include:
alpha==a(and similar) suffix normalizations.- Require version literals to start with a digit to avoid situations like
v0.1 != 0.1. - Revise PEP440 normalization rules and study which ones we should adopt.
- Propose a stricter subset of these rules to reduce ambiguity.
Examples
The ordering specification results in the following versions sorted in this way:
0.4
== 0.4.0
< 0.4.1.rc
== 0.4.1.RC # case-insensitive comparison
< 0.4.1+local # 'local' < 0
< 0.4.1+0.local # '0.local' < 0
< 0.4.1
== 0.4.1+0 # no local is the same as '+0'
< 0.4.1+1.local # '1.local' > 0
< 0.5a1
< 0.5b3
< 0.5C1
< 0.5
< 0.9.6
< 0.960923
< 1.0
< 1.1dev1 # special case 'dev'
< 1.1a1
< 1.1.0dev1
== 1.1.dev1 # 0 is inserted before string
< 1.1.a1
< 1.1.0rc1
< 1.1.0.0
== 1.1.0
== 1.1
< 1.1.post1 # special case 'post'
== 1.1.0post1
< 1.1post1
< 1996.07.12
< 1!0.4.1 # epoch increased from implicit 0
< 1!3.1.1.6
< 2!0.4.1
Local versions are not very common but there are some examples:
$ conda search "*[version='*+*']"
Loading channels: done
# Name Version Build Channel
py-sirius-ms 2.1+sirius6.0.3 pyhd8ed1ab_0 conda-forge
py-sirius-ms 2.1+sirius6.0.4 pyhd8ed1ab_0 conda-forge
py-sirius-ms 2.1+sirius6.0.5 pyhd8ed1ab_0 conda-forge
py-sirius-ms 2.1+sirius6.0.6 pyhd8ed1ab_0 conda-forge
py-sirius-ms 2.1+sirius6.0.7 pyhd8ed1ab_0 conda-forge
py-sirius-ms 2.1+sirius6.0.7 pyhd8ed1ab_1 conda-forge
py-sirius-ms 3.0+sirius6.1.0 pyhd8ed1ab_0 conda-forge
py-sirius-ms 3.0.1+sirius6.1.0 pyhd8ed1ab_0 conda-forge
py-sirius-ms 3.1+sirius6.1.1 pyhd8ed1ab_0 conda-forge
r-sirius-ms 2.1+sirius6.0.4 r44h57928b3_1 conda-forge
r-sirius-ms 2.1+sirius6.0.4 r44h694c41f_1 conda-forge
r-sirius-ms 2.1+sirius6.0.4 r44ha770c72_1 conda-forge
r-sirius-ms 2.1+sirius6.0.5 r44h57928b3_1 conda-forge
r-sirius-ms 2.1+sirius6.0.5 r44h694c41f_1 conda-forge
r-sirius-ms 2.1+sirius6.0.5 r44ha770c72_1 conda-forge
r-sirius-ms 2.1+sirius6.0.6 r44h57928b3_1 conda-forge
r-sirius-ms 2.1+sirius6.0.6 r44h694c41f_1 conda-forge
r-sirius-ms 2.1+sirius6.0.6 r44ha770c72_1 conda-forge
r-sirius-ms 2.1+sirius6.0.7 r44h57928b3_0 conda-forge
r-sirius-ms 2.1+sirius6.0.7 r44h57928b3_1 conda-forge
r-sirius-ms 2.1+sirius6.0.7 r44h694c41f_0 conda-forge
r-sirius-ms 2.1+sirius6.0.7 r44h694c41f_1 conda-forge
r-sirius-ms 2.1+sirius6.0.7 r44ha770c72_0 conda-forge
r-sirius-ms 2.1+sirius6.0.7 r44ha770c72_1 conda-forge
r-sirius-ms 3.0.1+sirius6.1.0 r44h57928b3_0 conda-forge
r-sirius-ms 3.0.1+sirius6.1.0 r44h694c41f_0 conda-forge
r-sirius-ms 3.0.1+sirius6.1.0 r44ha770c72_0 conda-forge
r-sirius-ms 3.1+sirius6.1.1 r44h57928b3_0 conda-forge
r-sirius-ms 3.1+sirius6.1.1 r44h694c41f_0 conda-forge
r-sirius-ms 3.1+sirius6.1.1 r44ha770c72_0 conda-forge
r-sirius-ms 3.1+sirius6.1.1 r45h57928b3_1 conda-forge
r-sirius-ms 3.1+sirius6.1.1 r45h694c41f_1 conda-forge
r-sirius-ms 3.1+sirius6.1.1 r45ha770c72_1 conda-forge
typst-test 0.0.0.post105+699b871 h6e96688_0 conda-forge
typst-test 0.0.0.post105+699b871 h6e96688_1 conda-forge
typst-test 0.0.0.post106+2b4e689 h6e96688_0 conda-forge
References
conda 25.7.xdocs on Version Ordering.- Comparison between
conda,rattlerandmambaparsers. - Draft CEP about disallowing
*in version literals.
Copyright
All CEPs are explicitly CC0 1.0 Universal.