Skip to content

Understanding Python Dependency Specifiers

requests[socks]>=2.32,<3; python_version >= "3.10" looks like one constraint. It is four instructions packed into one line:

requests [socks] >=2.32,<3 ; python_version >= "3.10"
└─ name  └extra┘ └versions┘   └────── marker ──────┘

Each part answers a different question. The name identifies the package, the extraA named set of optional dependencies requested during installation, usually for a package feature. The set can be empty. requests an optional feature, the version clauses limit acceptable releases, and the markerA condition that makes a dependency apply only to matching Python versions, operating systems, implementations, or processor architectures. decides whether the requirement applies in an environment.

The complete line is a dependency specifierA standardized string that names a package and can select optional features, limit acceptable versions, or restrict the environments where the package is needed. , also called a requirement specifier. PEP 508, the dependency-specification standard, defines its structure. PEP 440, the Python versioning standard, defines how its version clauses compare releases.

Read each requirement from left to right

Only the package name is required. The other parts appear in a fixed order:

Part Example Question it answers
Package name requests Which distribution is needed?
Extras [socks] Which optional features should it include?
Version specifier >=2.32,<3 Which releases may the resolver choose?
Environment marker ; python_version >= "3.10" Does this dependency apply here?

This order makes dense requirements easier to inspect. Find the semicolon first: everything after it is a condition on the requirement. Before the semicolon, square brackets request extras and comparison operators constrain versions.

Whitespace around the parts is mostly optional. requests[socks]>=2.32,<3 and requests [socks] >= 2.32, < 3 express the same requirement, though compact forms are more common in project files.

Choose acceptable package versions

A version specifier filters package releases. It does not tell the resolverThe part of a package manager that chooses a set of package versions satisfying every dependency constraint. which acceptable version to prefer, and it does not record the final choice. Package managers apply their own selection strategies; a lockfileA file that records the exact version of every installed package, so everyone working on the project gets identical installs. records the version selected for a reproducible installation.

Specifier Meaning Example result
>=2.0 Version 2.0 or later Accepts 2.0 and 3.1
<3 Earlier than version 3 Accepts 2.9; rejects 3.0
>=2,<3 Every comma-separated clause must match Accepts the 2.x range
!=2.1.4 Exclude one release Accepts 2.1.3 and 2.1.5
!=2.1.* Exclude one release series Rejects every 2.1 release
==2.1.4 Match one public version Also matches a local build such as 2.1.4+vendor.1
~=2.1 Compatible release at or after 2.1 Equivalent to >=2.1,==2.*
~=2.1.4 Compatible release at or after 2.1.4 Equivalent to >=2.1.4,==2.1.*

Commas mean and, not or. >=2,<3,!=2.1.4 keeps releases that satisfy all three clauses: at least 2, earlier than 3, and anything except 2.1.4.

Let ~= use the precision you write

The compatible-release operator often causes mistakes because its upper boundary depends on the number of version components:

  • ~=2.1 allows 2.1 through the rest of 2.x, but not 3.0.
  • ~=2.1.0 allows the 2.1 series, but not 2.2.0.

Adding .0 narrows the range. When that shorthand feels hard to review, write both bounds directly, such as >=2.1,<3 or >=2.1.0,<2.2.

Exclude known-bad releases with !=

An exclusion belongs beside the range it modifies:

urllib3>=2,<3,!=2.2.0

This says that the project supports urllib3 2.x except 2.2.0. It communicates a compatibility fact more accurately than raising the lower bound to >=2.2.1, which would reject older 2.x releases that still work.

Add package features with extras

An extra asks a package to install optional dependencies for a named feature. The package’s maintainers choose the extra names and what each one adds.

For example, requests[socks] installs Requests with its SOCKS support dependency. Multiple extras combine their dependency sets, as in package[postgres,cli]. They do not select variants of the package itself.

Extras are part of a package’s public installation interface. Packages declare extras in their distribution metadata; maintainers can configure them through [project.optional-dependencies] or backend-specific settings such as setuptools’ extras_require. Optional dependencies and dependency groups explains why an installable extra is different from a private test or lint dependency group.

The often-copied requests[security] example now demonstrates a separate lesson: an extra name can outlive the dependencies it once added. Requests still declares security for compatibility, but its current project metadata defines the extra as an empty list. Installing requests[security] does not harden a current Requests installation.

Install conditionally with environment markers

An environment marker makes the entire requirement conditional. If the expression after the semicolon evaluates to false, the installer ignores that requirement.

colorama>=0.4.6; sys_platform == "win32"

On Windows, the resolver considers Colorama releases at or after 0.4.6. On macOS and Linux, this requirement is ignored; another applicable requirement can still cause Colorama to be installed.

Common marker variables include:

Variable Example Use it for
python_version python_version >= "3.10" Python major and minor version
python_full_version python_full_version < "3.12.4" A condition that depends on the patch release
sys_platform sys_platform == "win32" Python’s platform identifier
platform_system platform_system == "Windows" Operating-system name
platform_machine platform_machine == "x86_64" CPU architecture reported by the platform
implementation_name implementation_name == "cpython" Python implementation

Marker values are strings, so quote the value on the right. The spelling and capitalization matter: sys_platform == "win32" and platform_system == "Windows" test related facts with different value conventions.

Join conditions with and or or, and add parentheses when the grouping is not obvious:

uvloop>=0.21; python_version >= "3.10" and sys_platform != "win32"

Markers describe where a dependency applies. They do not express which wheel file a resolver should choose; wheel compatibility tags and the package index handle that selection.

Combine the pieces without mixing their jobs

A realistic specifier can use versions, extras, and markers together:

requests[socks]>=2.32,<3; python_version >= "3.10"

Read it as a sequence:

  1. The dependency is Requests.
  2. Its socks extra adds optional SOCKS support dependencies.
  3. Any Requests release from 2.32 up to, but not including, 3 is acceptable.
  4. The complete requirement applies only on Python 3.10 or later.

Changing one part does not change the others. Removing [socks] leaves the version range and Python condition intact. Removing the marker makes the same package, extra, and range apply on every supported environment.

Use the same syntax in project files

pyproject.toml stores dependency specifiers as strings:

pyproject.toml
[project]
dependencies = [
    "requests[socks]>=2.32,<3",
    "colorama>=0.4.6; sys_platform == 'win32'",
]

A requirements.txt file uses one specifier per line without TOML’s surrounding string quotes:

requirements.txt
requests[socks]>=2.32,<3
colorama>=0.4.6; sys_platform == "win32"

At a shell prompt, quote the whole specifier so characters such as <, >, and ! reach pip or uv instead of being interpreted by the shell. Do not copy those outer shell quotes into requirements.txt.

Choose constraints for libraries and applications

For a library, version clauses describe the releases that its code supports. Prefer the widest range that has been tested. Add a lower bound when the code needs a feature introduced in that release, an upper bound when a known incompatibility requires it, and != when one release is broken.

For an application, dependency specifiers still describe compatibility. Put exact, resolved versions for direct and transitive dependenciesA package your dependency depends on. When you install requests, its own dependencies (urllib3, certifi, etc.) are transitive dependencies. in a lockfile. A range such as requests>=2.32,<3 and a lock entry for one exact Requests release solve different problems.

Exact == pins force downstream resolvers to accept only one release, which can conflict with other requirements. Use them in generated deployment files or lockfiles, where reproducing one environment is the goal.

Diagnose a requirement in four passes

When a dependency behaves unexpectedly, inspect each part separately:

  1. Name: Is this the distribution name used on the package index, which may differ from its Python import name?
  2. Marker: Does the marker evaluate to true for the target Python, operating system, implementation, and architecture?
  3. Versions: Is at least one published release inside every comma-separated clause and compatible with the target Python?
  4. Extras: Does the selected release publish that extra, and which dependencies does it add?

This order separates “the requirement does not apply” from “no acceptable release exists.” Resolver errors become easier to read once each symbol has only one job.

Last updated on