A Flask service with a hand-written requirements.txt listing eleven top-level packages. A data pipeline managed with Poetry and a 900-line poetry.lock. An internal tool still running on Pipenv with a Pipfile.lock nobody has touched since 2022. Ask a scanner to tell you "what's actually installed and is any of it vulnerable" and you get three completely different answers, because pip, Poetry, and Pipenv don't agree on what a dependency file even is. Snyk's Python support has to reconcile that mess with one CLI command and one vulnerability database. This piece walks through the publicly documented mechanics of how Snyk resolves Python dependency trees across the three most common tooling setups, why pip requires a fundamentally different approach than Poetry or Pipenv, and where teams commonly get tripped up when the resolution doesn't match what they expected to be scanned.
Why is Python dependency resolution harder than npm or Maven?
Because requirements.txt is not a lockfile, and Snyk can't extract a dependency tree from something that was never designed to contain one. npm ships package-lock.json and Maven ships a fully resolved POM graph, both of which pin every transitive dependency with exact versions in a machine-readable format. A typical requirements.txt, by contrast, is just a flat list of top-level packages — often with loose version specifiers like flask>=2.0 — with no record of what those packages pull in underneath them. Snyk's own documentation is explicit about this gap: for manifest-only Python projects, the tool cannot statically compute the full graph the way it does for JavaScript or Java, because the information simply isn't in the file. That single fact is what forces Snyk to use two structurally different resolution strategies depending on which Python tool a project uses.
How does Snyk resolve dependencies when a project only has a requirements.txt?
By actually building the environment rather than just reading a file. Because plain pip projects don't carry a resolved dependency graph, Snyk's CLI (through its snyk-python-plugin) falls back to what Snyk documents as dynamic resolution: it creates an isolated Python environment, installs the packages named in requirements.txt using pip's own resolver, and then inspects the resulting installed environment — effectively a pip freeze-style snapshot — to reconstruct the full transitive tree. This is also why the Snyk CLI has hard requirements around the local Python setup: it needs a compatible Python interpreter and pip version available on the machine or CI runner running the scan, and Snyk's docs let users point at a specific interpreter with the --command flag (for example --command=python3.11) when multiple versions are installed side by side.
The practical consequence is that scanning a pip project is not a passive read operation. It runs the same install step a developer or CI pipeline would run, which means it can reach out to PyPI, resolve version constraints the same way pip install would, and — because Python packages can ship a setup.py with arbitrary build-time code — execute code from third-party packages as part of getting an accurate dependency list. Snyk mitigates the operational overhead with options like --skip-unresolved to continue past packages it can't install, but the underlying model is still "install first, then inspect," not "parse and done."
How does Snyk handle full lockfiles from Poetry and Pipenv?
By parsing them directly, without installing anything, because both tools already produce a complete resolved graph. poetry.lock and Pipfile.lock are the Python ecosystem's answer to package-lock.json: every dependency and sub-dependency is listed with an exact pinned version, and in Poetry's case, cryptographic hashes for each resolved artifact. Snyk maintains dedicated plugins for each format — snyk-poetry-plugin for Poetry and its pip-family equivalent for Pipenv — that read the lockfile's dependency graph section directly, meaning a snyk test against a Poetry or Pipenv project is a static, offline-capable operation in a way that a bare pip project isn't.
This is a meaningful practical difference for anything security-sensitive: a locked Poetry or Pipenv project gives Snyk (and any other SCA tool) a deterministic, reproducible view of exactly what would be installed, matching what actually ends up in the container image or deployment artifact. A pip project without pinned transitive versions gives Snyk a best-effort snapshot of what pip's resolver picked at scan time — which can legitimately differ from what a resolver run a week earlier or later would have picked, since pip's dependency resolver (the newer resolver introduced in pip 20.3, released in November 2020) is satisfying the same loose constraints against whatever the current package index state is.
What are the practical gotchas developers hit when scanning Python projects with Snyk?
The most common one is a mismatch between the environment Snyk resolves against and the environment that actually ships to production. If a CI runner has a different Python minor version, a different pip version, or missing system libraries needed to build a C-extension package, the dynamic install step for a pip project can resolve a different tree — or fail outright — compared to a developer's laptop. Snyk's documentation recommends running scans from an environment as close as possible to the deployment target for exactly this reason, and recommends --all-projects combined with per-directory --file flags for monorepos with multiple requirements.txt files across services.
A second gotcha is that requirements files split across multiple sources — requirements.txt referencing requirements-dev.txt via -r, or private index URLs behind --extra-index-url — need matching network access and credentials at scan time for the install step to succeed, since Snyk is genuinely running pip against those sources. Teams with private PyPI mirrors have to configure the scanning environment's pip configuration (pip.conf or environment variables) identically to their build environment, or the resolution silently falls back to public PyPI and produces a tree that doesn't match reality. Finally, because setuptools and older packaging metadata sometimes only expose a subset of runtime dependencies through install_requires, extras declared with bracket syntax (like package[extra]) need to be captured in the manifest for the installed extras' dependencies to appear in the resolved tree at all — a subtlety that's easy to miss when auditing scan coverage.
How Safeguard Helps
Understanding how a scanner resolves your dependency tree matters because the resolution method is the ceiling on how trustworthy the resulting vulnerability report can be — a tool that has to guess at a pip project's transitive dependencies is only as good as the environment it guessed against. Safeguard approaches Python supply chain visibility from the artifact side of that same problem: rather than relying solely on dynamic resolution against requirements.txt at scan time, Safeguard correlates resolved dependency data with what's actually present in build outputs and container images, so the security team isn't dependent on the scanning environment perfectly mirroring production to get an accurate picture.
For teams running a mix of pip, Poetry, and Pipenv across different services — a common state for organizations that have grown through acquisitions or split monoliths into microservices — Safeguard also helps standardize policy enforcement across that fragmentation. Instead of having Poetry's cryptographically-hashed lockfiles held to one standard of confidence and unpinned pip requirements files held to another by default, Safeguard's supply chain monitoring surfaces where dependency pinning is inconsistent across a fleet of Python services and flags unpinned or unhashed manifests as a distinct risk category worth remediating on its own, separate from whatever vulnerabilities happen to be in the resolved packages that week. That distinction — knowing not just what's vulnerable, but how confidently you actually know what's installed — is where a lot of real-world Python supply chain risk quietly lives.