Versions & Dependency Specifiers

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the Python Reference bibliography.

Every requirement you write, whether in [project] dependencies, a requirements.txt file, on the pip install command line or in poetry add, is built from the two grammars on this page: PEP 440 versions and specifiers, and PEP 508 requirement strings. The examples were run on 2026-10-22 with CPython 3.14.6, pip 26.2.2, packaging 26.3 (the library that pip vendors and that implements both specifications) and poetry-core 2.5.0.

Version numbers (PEP 440)

A public version identifier has up to five parts, in this order:

[N!]N(.N)*[{a|b|rc}N][.postN][.devN][+local]
 ^    ^        ^         ^      ^      ^
 |    |        |         |      |      local version label (never on PyPI)
 |    |        |         |      developmental release
 |    |        |         post-release
 |    |        pre-release: alpha, beta, release candidate
 |    release segment: 1.4, 1.4.2, 2026.10 …
 epoch (discouraged)
  • Release segment. One or more numbers. The specification does not impose semantic versioning; 1.4.2, 2026.10 and 3 are all valid. Trailing zeros are not significant in comparisons: 1.0 == 1.0.0.

  • Pre-releases a, b, rc come before the final release: 1.0a1 < 1.0b1 < 1.0rc1 < 1.0.

  • Post-releases .postN fix something that is not code (a packaging mistake, the docs) and sort after the release.

  • Developmental releases .devN sort before the pre-release or release they are attached to. Since May 2025 the specification states that dev releases count as pre-releases for specifier handling.

  • Local version labels +… identify a locally patched build (1.0+ubuntu.1). They sort after the public version, are ignored by most specifiers, and PyPI rejects uploads that carry one.

  • Epochs N! restart ordering when a project changes scheme (2014.04 → 1!1.0). Their use has been discouraged since January 2026: many downstream packagers do not support them, and the specification now recommends continuing with larger numbers in epoch 0 instead (for example, 2026.x followed by 3000.x).

Normalization

Many spellings are accepted and normalized to one canonical form. Tools compare normalized versions, PyPI displays them, and the wheel file name uses them:

from packaging.version import Version

for raw in ["1.0", "1.0.0", "v1.0", "1.0-RC1", "1.0.post1", "1.0-1", "1.0.dev3", "1!2.0", "1.0+ubuntu.1", "1.0a1.post2.dev3"]:
    v = Version(raw)
    print(f"{raw:18} -> {str(v):16} pre={v.pre} post={v.post} dev={v.dev} local={v.local} epoch={v.epoch}")

print(sorted(["1.0", "1.0.post1", "1.0rc1", "1.0.dev1", "1.0a1", "1.0+local", "0.9", "1!0.1"], key=Version))
1.0                -> 1.0              pre=None post=None dev=None local=None epoch=0
1.0.0              -> 1.0.0            pre=None post=None dev=None local=None epoch=0
v1.0               -> 1.0              pre=None post=None dev=None local=None epoch=0
1.0-RC1            -> 1.0rc1           pre=('rc', 1) post=None dev=None local=None epoch=0
1.0.post1          -> 1.0.post1        pre=None post=1 dev=None local=None epoch=0
1.0-1              -> 1.0.post1        pre=None post=1 dev=None local=None epoch=0
1.0.dev3           -> 1.0.dev3         pre=None post=None dev=3 local=None epoch=0
1!2.0              -> 1!2.0            pre=None post=None dev=None local=None epoch=1
1.0+ubuntu.1       -> 1.0+ubuntu.1     pre=None post=None dev=None local=ubuntu.1 epoch=0
1.0a1.post2.dev3   -> 1.0a1.post2.dev3 pre=('a', 1) post=2 dev=3 local=None epoch=0
['0.9', '1.0.dev1', '1.0a1', '1.0rc1', '1.0', '1.0+local', '1.0.post1', '1!0.1']

The leading v, upper case and - separators are dropped (1.0-1 is an implicit post-release), and 1!0.1 sorts after every epoch-0 version. Versions that cannot be parsed at all ("legacy versions" such as 1.0-final-2) were deprecated in pip 23.2 and removed in pip 24.1: pip now ignores distributions with such versions.

Version specifiers

A specifier is a comma-separated list of clauses, all of which must match (a logical AND; there is no OR).

Operator Name Meaning

~=

compatible release

~=1.4.2 means >=1.4.2, ==1.4.*; ~=1.4 means >=1.4, ==1.*. Drop the last component, keep the rest fixed.

==

version matching

Equal after normalization and zero padding. Without a wildcard, a local label on the candidate is ignored, so ==1.0 matches 1.0+local.

==X.*

prefix match

==1.4.* matches 1.4, 1.4.0, 1.4.7, … but not 1.5.

!=

version exclusion

The inverse of ==, wildcards included: !=1.4.*.

<=, >=

inclusive ordered comparison

Normal version ordering. <=2.0 does not match 2.0.post1.

<, >

exclusive ordered comparison

<2.0 excludes the pre-releases of 2.0 too; >1.0 excludes the post-releases of 1.0 unless the specifier is itself a post-release.

===

arbitrary equality

Plain, case-insensitive string comparison with no normalization: ===1.0 matches 1.0 but not 1.0.0. An escape hatch for unparseable legacy versions; avoid it.

The behaviour of each operator, checked with packaging:

from packaging.specifiers import SpecifierSet

for spec, versions in [
    ("~=1.4.2", ["1.4.1", "1.4.2", "1.4.9", "1.5.0"]),
    ("~=1.4", ["1.3", "1.4", "1.9", "2.0"]),
    ("==1.4.*", ["1.4", "1.4.7", "1.5"]),
    ("!=1.4.*", ["1.4.2", "1.5"]),
    (">=1.0,<2", ["1.0", "1.9.9", "2.0.0rc1", "2.0"]),
    ("==1.0", ["1.0", "1.0.0", "1.0+local"]),
    ("===1.0", ["1.0", "1.0.0"]),
    ("<=2.0", ["2.0", "2.0.post1"]),
    (">1.0", ["1.0.post1", "1.1"]),
]:
    s = SpecifierSet(spec)
    print(f"{spec:10}", {v: s.contains(v) for v in versions})
~=1.4.2    {'1.4.1': False, '1.4.2': True, '1.4.9': True, '1.5.0': False}
~=1.4      {'1.3': False, '1.4': True, '1.9': True, '2.0': False}
==1.4.*    {'1.4': True, '1.4.7': True, '1.5': False}
!=1.4.*    {'1.4.2': False, '1.5': True}
>=1.0,<2   {'1.0': True, '1.9.9': True, '2.0.0rc1': False, '2.0': False}
==1.0      {'1.0': True, '1.0.0': True, '1.0+local': True}
===1.0     {'1.0': True, '1.0.0': False}
<=2.0      {'2.0': True, '2.0.post1': False}
>1.0       {'1.0.post1': False, '1.1': True}

Pre-releases

Pre-releases (and dev releases) are excluded from every specifier unless one of three things is true: the pre-release is already installed, the user asked for pre-releases explicitly, or only pre-releases satisfy the specifier. A specifier that itself names a pre-release (>=2.0b1) also opts in.

from packaging.specifiers import SpecifierSet

s = SpecifierSet(">=1.0")
print("pre-releases excluded by default:", list(s.filter(["1.0", "2.0b1"])), "| allowed:", list(s.filter(["1.0", "2.0b1"], prereleases=True)))
print("only pre-releases available:", list(s.filter(["2.0b1", "2.0rc1"])))
print("explicit pre in spec:", SpecifierSet(">=2.0b1").contains("2.0b2"))
pre-releases excluded by default: ['1.0'] | allowed: ['1.0', '2.0b1']
only pre-releases available: ['2.0b1', '2.0rc1']
explicit pre in spec: True

The switches for "accept pre-releases" are pip install --pre (PIP_PRE=1), Poetry’s allow-prereleases = true on a dependency and poetry add --allow-prereleases. pip 26.0 added --all-releases and --only-final to control pre-releases per package; see pip: Installing Packages.

Requirement strings (PEP 508)

A requirement names a distribution and optionally restricts it:

name [ "[" extras "]" ] ( version-specifier | "@" url ) [ ";" marker ]
from packaging.requirements import Requirement

r = Requirement('rich[jupyter]>=13; python_version >= "3.10" and sys_platform != "emscripten"')
print(r.name, r.extras, r.specifier, r.marker, sep=" | ")
r2 = Requirement("birdtally @ https://example.org/dist/birdtally-0.1.0-py3-none-any.whl")
print(r2.name, r2.url)
rich | {'jupyter'} | >=13 | python_version >= "3.10" and sys_platform != "emscripten"
birdtally https://example.org/dist/birdtally-0.1.0-py3-none-any.whl

The specifier may be wrapped in parentheses, rich (>=13), which is the form poetry-core writes into metadata and the form poetry add writes into [project] dependencies.

Extras

name[extra1,extra2] selects optional dependency sets. Since PEP 685 extra names are normalized like project names (lower case, runs of -_. become -), so birdtally[Pretty] and birdtally[pretty] are the same request. Inside metadata an extra appears as a marker, extra == "pretty", which is evaluated against the extras being installed.

Source: PyPA: Extras.

Environment markers

A marker after ; decides whether the requirement applies on the current machine. The marker variables are os_name, sys_platform, platform_machine, platform_python_implementation, platform_release, platform_system, platform_version, python_version, python_full_version, implementation_name and implementation_version, plus extra. PEP 751 added extras and dependency_groups for use in lock files only: they are sets, tested with in. The values for the machine that ran these examples:

import json
from packaging.markers import Marker, default_environment

print(json.dumps(default_environment(), indent=2))
print(Marker('python_version < "3.11"').evaluate())
print(Marker('extra == "Pretty"').evaluate({"extra": "pretty"}))
print(Marker('"test" in dependency_groups').evaluate({"dependency_groups": {"test"}}, context="lock_file"))
{
  "implementation_name": "cpython",
  "implementation_version": "3.14.6",
  "os_name": "posix",
  "platform_machine": "x86_64",
  "platform_release": "6.18.44-fc-v140",
  "platform_system": "Linux",
  "platform_version": "#1 SMP PREEMPT_DYNAMIC @0",
  "python_full_version": "3.14.6",
  "platform_python_implementation": "CPython",
  "python_version": "3.14",
  "sys_platform": "linux"
}
False
True
True

python_version is only major.minor; compare python_full_version when the patch level matters. Markers are combined with and, or and parentheses, and versions are compared with the PEP 440 operators (string comparison is used for the other variables). pip reports a requirement whose marker does not match instead of failing:

python -m pip install --dry-run "rich~=13.7" 'colorama; sys_platform == "win32"'
Ignoring colorama: markers 'sys_platform == "win32"' don't match your environment
…
Would install Pygments-2.21.0 markdown-it-py-4.2.0 mdurl-0.1.2 rich-13.9.4

Direct references

name @ url points at one file or repository instead of an index: a wheel or sdist URL, file:///… for a local path, or a VCS URL such as birdtally @ git+https://example.org/birdtally.git@v0.1.0. Direct references are allowed in applications and lock files. PyPI rejects uploads whose Requires-Dist contains one, because an installer could not resolve it reproducibly from the index. How pip and Poetry handle them is on pip: VCS, Direct URL & Local Project Installs and Poetry: Dependency Specification.

Errors you will see

A requirement that matches nothing on the index:

python -m pip install --dry-run "rich>=99"
ERROR: Ignored the following yanked versions: 12.1.0
ERROR: Could not find a version that satisfies the requirement rich>=99 (from versions: 0.1.0, 0.2.0, … 14.3.4, 15.0.0)
ERROR: No matching distribution found for rich>=99

A name that does not exist gives (from versions: none), which is also what you see when the name is misspelled or the index is wrong. A specifier that is not PEP 440 fails before anything is downloaded:

python -m pip install --dry-run "rich^13"
ERROR: Invalid requirement: 'rich^13': Expected semicolon (after name with no version specifier) or end
    rich^13
        ^

pip and Poetry: translating constraints

Poetry accepts every PEP 440 specifier and adds two shorthands of its own, the caret and the tilde, which are not PEP 508 and therefore only valid in [tool.poetry] tables and on the poetry add command line. Poetry translates them to PEP 440 ranges when it writes metadata. The translations below were printed by poetry-core 2.5.0’s own parser (poetry.core.constraints.version.parse_constraint):

Poetry PEP 440 equivalent Rule

^1.2.3

>=1.2.3,<2.0.0

Caret: allow updates that do not change the left-most non-zero component.

^1.2

>=1.2,<2.0

^1

>=1,<2

^0.2.3

>=0.2.3,<0.3.0

For 0.x the minor is treated as the breaking component.

^0.0.3

>=0.0.3,<0.0.4

And for 0.0.x the patch.

^0.0

>=0.0,<0.1

^0

>=0,<1

~1.2.3

>=1.2.3,<1.3.0

Tilde: with a minor given, only patch updates.

~1.2

>=1.2,<1.3

~1

>=1,<2

With only a major, minor and patch updates.

1.2.*

==1.2.*

Wildcards are plain PEP 440.

~=1.2

>=1.2,<2.0

PEP 440 compatible release: careful, ~=1.2 is not ~1.2.

*

any version

poetry add rich writes "rich (>=15.0.0,<16.0.0)" into [project] dependencies, which is the caret range of the latest version spelled in PEP 508; see Poetry: Dependency Specification. Putting the shorthand into [project] fails Poetry’s own check:

# pyproject.toml: dependencies = ["rich^13"]
poetry check
The requirement is invalid: Unexpected character at column 5

rich^13
    ^

Why upper bounds hurt libraries

An application controls its whole environment and pins exact versions in a lock file. A library does not: its users install it next to everything else they use, and the resolver must find one version of every package that satisfies all constraints at once. Every upper bound a library declares (rich<14, or the implicit one in ^13.0) is a statement that the library is broken with the next major release of that dependency, made before that release exists. If it is wrong, users are stuck on old versions or get an unresolvable conflict that only a new release of your library can fix.

  • Prefer lower bounds only (rich>=13) in libraries, and add an upper bound only when you know a release breaks you.

  • Avoid requires-python upper bounds entirely; they make resolvers pick old releases or fail.

  • Poetry’s caret default writes an upper bound for every dependency you add. Poetry’s own FAQ discusses the trade-off; for a library, edit the generated rich (>=15.0.0,<16.0.0) to rich (>=15.0.0) unless you have a reason.

  • Applications should pin through a lock file, not through tight ranges in metadata.