The ~= operator means compatible release: it accepts newer versions that should stay compatible with the one you name, such as bug fixes, but not the next major or minor step you did not choose. Use it in library metadata, use == for deployable pins, and keep the two jobs separate.
The operators, in plain words
The Python packaging specification lists the operators a version clause can use: ~= for compatible release, == for version matching, != for exclusion, <= and >= for inclusive comparison, < and > for exclusive comparison, and === for arbitrary equality. Clauses are separated by commas and the comma works as a logical and, so every clause must match.
The specification's own example is ~= 0.9, >= 1.0, != 1.3.4.*, < 2.0. When several versions match, the preferred one is the latest.
| Style | What it expresses | Fits |
|---|---|---|
| Exact with == | One version only | Applications, deploys, hash-checked installs |
| Compatible with ~= | This release or later fixes in the same series | Libraries and tools you publish |
| Lower bound with >= | Anything newer | Minimum supported version only |
| Open, no clause | Anything at all | Prototypes, never production |
Two jobs, two files
Python projects mix up two different questions. What does my package need in order to work? And what exact set did I test and deploy?
The first belongs in project metadata and should be as loose as is honest, because a library that pins too tightly makes life hard for everyone who installs it next to other packages. The second belongs in a generated, fully pinned file, usually with hashes. Pip's documentation requires exact pins for hash-checking mode for precisely this reason: a loose specifier can match a new release and cause a surprising mismatch.
Choosing for an application
For something you deploy, pin everything with == in a lock output and keep a short, readable list of direct requirements next to it. Update the pinned file on purpose, in a pull request, with tests.
Poetry takes a similar split approach in its own format. Its caret requirements allow updates that do not change the left-most non-zero digit, so a requirement of 2.13.0 with a caret may move to 2.14.0 but not 3.0.0, while the lock file records the exact result. Poetry also notes that caret and tilde requirements are not supported in the standard project dependencies table and are converted when you add them, so check how your tool treats the field.
Choosing for a library
For a package other people install, state the range you have actually tested. A compatible release clause is a reasonable way to say "I tested with this series and expect fixes to be safe". Do not pin to one exact version, because you will cause resolver conflicts downstream. Do not leave it fully open either, because you cannot promise it works with a release that did not exist yet.
Checks worth running
- Search your requirements for lines with no operator at all.
- Confirm production installs use a fully pinned file.
- Confirm that file includes transitive dependencies.
- Review every change to the pinned file as you would review code.
Where Safeguard fits
Safeguard's dependency scanning and SBOM generation can inventory the packages a Python project actually resolves, so you can compare them with what your ranges permit. A free scan is available at gold.safeguard.sh/scan.
Quick check
Is ~= the same as a caret in npm?
Not exactly. Compatible release in Python is defined by the version you write, and the packaging specification has its own rules. Check the specification rather than assuming npm behavior.
Should a library pin with ==?
Rarely. Exact pins in a library cause resolver conflicts for the people who install it. Reserve exact pins for the application that deploys.
Sources
- Python Packaging: Version specifiers
- pip documentation: Secure installs
- Poetry: Dependency specification
Last reviewed: 19 October 2026.