Skip to content

Fix: PDM Not Working — Lock File Errors, PEP 582 Confusion, and Script Issues

FixDevs · (Updated: )

Part of:  Python Errors

Quick Answer

How to fix PDM errors — pdm install fails resolving dependencies, lock file outdated warning, __pypackages__ vs venv confusion, pdm run script not found, build backend missing, and dependency groups setup.

The Error

You install PDM and try to install your project — resolution fails:

$ pdm install
ERROR: Unable to find a resolution for dependency requirement
The constraints are:
  - flask >= 3.0
  - django >= 5.0  # Conflict — both pull incompatible Werkzeug

Or the lock file warns about being outdated every time:

$ pdm install
WARNING: Lock file is out of date. Run `pdm lock` to update it.
# But you just ran pdm lock yesterday

Or PEP 582’s __pypackages__ confuses your IDE:

import requests   # Works on CLI
# IDE: "requests" cannot be resolved

Or pdm run can’t find a script you defined:

[tool.pdm.scripts]
test = "pytest tests/"
$ pdm run test
ERROR: Command 'test' not found

Or the build backend isn’t set up correctly:

$ pdm build
ERROR: Cannot find a build backend

PDM is a Python package manager that started by championing PEP 582 (__pypackages__ directory instead of virtual envs), but now supports both venv and PEP 582 modes. The dependency resolver is fast (Rust-backed via unearth), and the dependency groups feature is more flexible than Poetry’s extras. But the PEP 582 model and PDM-specific config patterns produce specific failures. This guide covers each.

Why This Happens

PDM defaults to PEP 582 mode — packages install into __pypackages__/X.Y/lib/ in the project root rather than a venv. This bypasses the typical “activate venv” step but requires IDE support (Python extension knows to look in __pypackages__). Without that support, IDEs show every import as unresolved even when CLI commands work.

The lock file (pdm.lock) records exact versions. Changes to pyproject.toml invalidate it — PDM warns you to re-lock. But the warning persists even after pdm lock if the lock file’s content hash doesn’t match the dependency spec hash, which can happen with multi-strategy resolutions.

In Production: Incident Lens

When PDM fails on a developer laptop, you fix it locally and move on. When it fails in CI or in a production build pipeline, every deploy freezes until you ship a fix. The blast radius is the whole engineering org — backend, frontend bundles that depend on Python tooling, infra Lambdas, scheduled batch jobs. Treat PDM failures in CI as a Sev-2 from the first red build.

Monitoring signals. Watch for: CI step pdm install --frozen-lockfile failing repeatedly across unrelated PRs (resolver regression), build time creeping past historical p95 (solver retries before fail), and pdm publish step erroring on PyPI rate limits. A GitHub Actions job that previously took 90 seconds and now takes 8 minutes is almost always a resolver thrash — PDM is retrying with widened constraints. If you ship a Sentry release annotation on each deploy, sudden gaps between annotations correlate with a frozen pipeline.

Blast radius and triage. A failed pdm install blocks deploys for every service that shares the affected dependency tree. Hotfixes ship from the same pipeline — so a stuck resolver also blocks security patches. Triage by: (1) pinning the last green lockfile commit in CI as a fallback image so emergency deploys still ship, (2) reverting the dep bump that triggered the regression, (3) opening a non-blocking PR with pdm lock --update-reuse to converge later. Do not let the team pip install around PDM in a panic — that creates ghost dependencies the lockfile doesn’t know about, and the next deploy fails identically.

Recovery sequence. pdm lock --refresh first (re-resolve against the same constraints, no upgrades), then pdm install --frozen-lockfile in a clean container that mirrors CI. If both pass locally but CI still fails, the issue is network: PyPI mirror unreachable, private index token expired, or setup-pdm cache pointing at a corrupted tarball. Clear the cache key and rerun. For air-gapped or restricted networks, mirror conflicts often show up the same way — see the cross-tool symptom set in pip could not build wheels.

Postmortem preventives. Run pdm install --frozen-lockfile (not plain pdm install) in every CI job — this fails loudly on lockfile drift instead of silently regenerating against a stale lock. Add a nightly scheduled job that runs pdm lock --check against main; if the lock drifts from pyproject.toml, open an issue before a real deploy hits it. Pin the setup-pdm action to a SHA, not a tag, so a transitive action update can’t break your pipeline overnight.

Fix 1: Installing PDM

# Standalone (recommended — isolated from project)
pipx install pdm

# Or via uv
uv tool install pdm

# Or pip (less isolated)
pip install --user pdm

# Or via Homebrew
brew install pdm

Verify:

pdm --version
pdm python list   # Show available Pythons PDM can use

Initialize a project:

pdm init
# Interactive prompts: name, version, Python version, license, etc.

This creates pyproject.toml with PDM-specific sections.

Common Mistake: Installing PDM via pip install pdm into a project venv. When PDM manages that same venv, it can uninstall itself during dependency resolution. Always install PDM globally via pipx, uv, or brew.

Fix 2: Venv vs PEP 582 (__pypackages__)

PDM has two install modes:

PEP 582 (default) — __pypackages__:

my-project/
├── pyproject.toml
├── pdm.lock
└── __pypackages__/
    └── 3.12/
        ├── lib/    (installed packages here)
        └── bin/
pdm install   # Installs into __pypackages__
pdm run python my_script.py   # Runs using PEP 582 lookup

Virtualenv mode:

pdm config python.use_venv true   # Switch globally
pdm venv create
pdm install   # Now installs into .venv
my-project/
├── pyproject.toml
├── pdm.lock
└── .venv/
    └── (standard venv layout)

When to use which:

ModeProsCons
PEP 582No activate needed, project-localTooling support varies, niche standard
venvUniversal tool support, well-understoodNeed to activate or use pdm run

Pro Tip: Use venv mode (pdm config python.use_venv true) unless you specifically want PEP 582 semantics. Every IDE, debugger, profiler, and CI tool understands venvs. PEP 582 was rejected as a Python standard in 2023 — tooling support won’t expand further. Venv mode gives you PDM’s resolver and project model without the PEP 582 compatibility tax.

Fix 3: Minimal pyproject.toml for PDM

[project]
name = "mypackage"
version = "0.1.0"
description = "My package"
authors = [
    {name = "Your Name", email = "[email protected]"},
]
dependencies = [
    "requests>=2.31",
    "pydantic>=2.0",
]
requires-python = ">=3.10"
license = {text = "MIT"}
readme = "README.md"

[project.optional-dependencies]
test = ["pytest>=7", "pytest-cov"]
docs = ["sphinx", "furo"]

[project.scripts]
mycli = "mypackage.cli:main"

[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"

[tool.pdm]
distribution = true   # This package builds for distribution (PyPI)

[tool.pdm.scripts]
test = "pytest tests/"
lint = "ruff check ."
type = "mypy src/"

[tool.pdm.dev-dependencies]
dev = [
    "ruff>=0.5",
    "mypy>=1.10",
    "pytest>=7",
]

pdm-backend is PDM’s build backend — analogous to hatchling for Hatch or setuptools for setuptools-based projects.

distribution = true marks this as a distributable package (vs. an application). PDM behaves slightly differently for distributable vs application projects (lockfile precision, build behavior).

Common Mistake: Omitting the [build-system] section. Without it, pdm build fails with “cannot find a build backend.” Even if you only use PDM for dependency management and never publish, include [build-system] — it makes pdm install work correctly for editable installs.

Fix 4: Dependency Groups

PDM has three categories of dependencies:

[project]
dependencies = ["requests"]   # Runtime, always installed

[project.optional-dependencies]
test = ["pytest"]              # Optional — pip install "mypkg[test]"
docs = ["sphinx"]

[tool.pdm.dev-dependencies]
dev = ["ruff", "mypy"]          # PDM-specific, never published
lint = ["ruff"]
typing = ["mypy", "types-requests"]

Install groups:

pdm install                       # Default group (production deps only)
pdm install -G test               # Production + test group
pdm install -G test -G docs       # Multiple groups
pdm install -G:all                # All groups
pdm install -d                    # Include dev-dependencies
pdm install --without test        # All groups except test

Dev-dependencies vs optional-dependencies:

  • optional-dependencies — Standard, published to PyPI in package metadata. Users can pip install mypkg[test].
  • tool.pdm.dev-dependencies — PDM-specific, NOT in published metadata. Local-only.

Use optional-deps for features end users opt into; use dev-deps for tools only developers need (linters, formatters).

Pro Tip: Group dev dependencies by purpose, not by tool. lint, typing, test, docs — easy to install only what’s needed for a specific workflow:

pdm install -d -G lint                # Just for fast lint check in CI
pdm install -d -G test -G typing      # For local test runs

Fix 5: Lock File Issues

$ pdm install
WARNING: Lock file is out of date.

The lockfile’s recorded content_hash doesn’t match the current pyproject.toml dependency spec.

Update the lockfile:

pdm lock                          # Re-resolve and update pdm.lock
pdm lock --update-reuse           # Only update changed deps, keep others pinned
pdm lock --upgrade                # Upgrade all to latest compatible versions
pdm lock --upgrade requests       # Upgrade just requests

Cross-platform lock files — PDM defaults to “strategy” mode where the lock works across platforms:

pdm lock --strategy cross_platform   # (Default since 2.x)
pdm lock --strategy direct_minimal_versions   # Resolve to minimum versions

Multi-strategy locking — combine strategies:

[tool.pdm]
lock-strategy = "cross_platform,inherit_metadata"

Common Mistake: Committing pdm.lock and pyproject.toml separately. The lock file’s content hash is based on pyproject.toml — if you bump a dep version in pyproject.toml and forget to update the lock, every PDM command warns. Always commit both files together after any dependency change.

pdm install --frozen-lockfile for CI — fail if lockfile is outdated:

pdm install --frozen-lockfile
# Errors instead of warning, prevents accidental installs against stale lock

Fix 6: Scripts and Tasks

[tool.pdm.scripts]
# Simple
test = "pytest tests/"

# With args via {args}
test-verbose = "pytest tests/ -v"
test-file = "pytest {args}"

# Multi-line shell script
setup = {shell = '''
    echo "Setting up..."
    pdm install -d
    pdm run pre-commit install
'''}

# Composite — run other scripts in sequence
all = {composite = ["lint", "type", "test"]}

# With environment variables
serve = {cmd = "uvicorn main:app", env = {ENV = "development"}}

# Help text (shows in `pdm run --list`)
test = {cmd = "pytest tests/", help = "Run the test suite"}

Run scripts:

pdm run test
pdm run all                       # Runs lint, type, test in sequence
pdm run test-file tests/unit/     # Pass args
pdm run --list                    # List all defined scripts

Common Mistake: Defining test = "pytest" and trying pdm run pytest. The script name is test, not pytest. To run the actual pytest binary from the env, use pdm run pytest only if pytest is exposed as a CLI. For scripts you defined, use the script name.

Pre-commit hooks via PDM:

[tool.pdm.scripts]
pre_install = {composite = ["pdm run lint", "pdm run test"]}

These run before pdm install — useful for blocking installs that would break the project.

Fix 7: Resolver Conflicts and Strategy

ERROR: Unable to find a resolution

PDM’s resolver couldn’t find versions of all packages that satisfy every constraint.

Diagnose:

pdm install --verbose             # Show resolution steps
pdm lock --verbose                # See where conflicts occur

Loosen constraints:

# Too strict — likely the source of conflict
dependencies = [
    "flask==3.0.1",
    "werkzeug==3.0.0",
]

# Better — let resolver pick compatible versions
dependencies = [
    "flask>=3.0",
    # werkzeug pulled as flask's dep; don't pin separately
]

Override a transitive dependency:

[tool.pdm.resolution.overrides]
urllib3 = ">=2.0"
certifi = ">=2024.0"

Forces PDM to use those versions even if a dep requests something else. Use sparingly — it’s bypassing the package author’s pin for a reason.

Exclude a dependency:

[tool.pdm.resolution.excludes]
some-transitive-dep = "*"

This prevents PDM from installing it. Useful when a transitive dep is broken on your platform.

For dependency resolution patterns in other package managers, see Poetry dependency conflict and uv not working.

Still Not Working? Production-only resolver failure

You see green builds in dev, red builds in CI, and the only difference is the machine. Three causes account for most of it. First, architecture mismatch — dev is osx-arm64, CI is linux-x64, and a transitive wheel only exists for one. Reproduce locally with docker run --rm -v $PWD:/app -w /app python:3.12 pdm install --frozen-lockfile to match the CI architecture exactly. Second, private index auth that works via OS keyring on dev but not via env var in CI — pdm config writes to ~/.config/pdm/config.toml and CI never reads it; export PDM_PYPI_USERNAME and PDM_PYPI_PASSWORD in the runner environment. Third, Python patch version drift — dev has 3.12.4, CI uses actions/setup-python with python-version: "3.12" which silently rolls to 3.12.7 and pulls a different lockfile branch on platforms that pin to patch.

Still Not Working? Lockfile merge conflicts that won’t resolve

Two PRs both touch pdm.lock and git merge produces a textual conflict you can’t reasonably hand-edit. Do not try. Resolve pyproject.toml first, accept either side’s pdm.lock as a starting point, then run pdm lock --update-reuse to converge against the merged manifest. Commit the regenerated lock as a single follow-up. The same pattern applies to rebase: take the incoming pdm.lock, then re-lock. Hand-merging a JSON-ish lockfile produces silent dep drift that ships to prod weeks later.

Still Not Working? __pypackages__ ghost installs

You switched to venv mode but __pypackages__/ still exists from before. pdm install writes to .venv, but your editor’s “extra paths” config still points at __pypackages__/3.12/lib, so the IDE shows stale package versions and import autocomplete from a six-month-old build. Delete __pypackages__/ after any mode switch and remove python.analysis.extraPaths entries that reference it. Same fix applies if you ever ran pdm install --no-isolation against a system Python — purge the artifact directory before trusting the next install.

Fix 8: Publishing to PyPI

# Build wheel and sdist
pdm build
# Output: dist/mypackage-0.1.0.tar.gz and dist/mypackage-0.1.0-py3-none-any.whl

# Publish to PyPI
pdm publish

# Or publish to TestPyPI first
pdm publish --repository testpypi

Configure credentials:

# Use PYPI_TOKEN env var (recommended)
export PYPI_TOKEN=pypi-xxx
pdm publish

# Or via PDM config (stored in ~/.config/pdm/config.toml)
pdm config repository.pypi.username __token__
pdm config repository.pypi.password pypi-xxx

Trusted publishers for GitHub Actions (no token in secrets):

# .github/workflows/publish.yml
permissions:
  id-token: write

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pdm-project/setup-pdm@v4
      - run: pdm build
      - uses: pypa/gh-action-pypi-publish@release/v1

PyPI supports OIDC trusted publishers — no PYPI_TOKEN needed if configured in PyPI project settings.

Pro Tip: For a publishing workflow, use pdm bump:

pdm bump patch          # 0.1.0 → 0.1.1
pdm bump minor          # 0.1.0 → 0.2.0
pdm bump major          # 0.1.0 → 1.0.0
pdm bump pre alpha      # 0.1.0 → 0.1.0a0
pdm bump 0.5.0          # Set explicit

Combined with CI on git tags, this gives a clean release flow without manual version edits.

Still Not Working?

PDM vs Poetry vs uv vs Hatch

  • PDM — PEP 582 pioneer (deemphasized now), strong resolver, dependency groups. Best for projects that want non-venv mode or fine-grained dep groups.
  • Poetry — Most mature, single-config opinionated. Best for production apps.
  • uv — Rust-based, dramatically faster. Best when speed matters.
  • Hatch — PyPA-official, env matrix support.

All four solve overlapping problems. For new projects in 2025, uv has the strongest momentum. PDM is a solid choice if you specifically want its dependency groups model or have an existing PDM project.

IDE Configuration for PEP 582

For VS Code with PEP 582:

// .vscode/settings.json
{
    "python.analysis.extraPaths": [
        "__pypackages__/3.12/lib"
    ],
    "python.defaultInterpreterPath": "/usr/local/bin/python3.12"
}

For PyCharm with PEP 582:

  • Settings → Project → Python Interpreter → Add → System Interpreter
  • Add __pypackages__/3.12/lib to interpreter paths

Or switch to venv mode (pdm config python.use_venv true) — VS Code and PyCharm auto-detect .venv/ without configuration.

Importing into Other Tools (tox, nox)

# tox.ini
[testenv]
allowlist_externals = pdm
commands = pdm run pytest

Or use pdm export to generate requirements.txt:

pdm export -o requirements.txt --without-hashes
pdm export -dG test -o requirements-dev.txt --without-hashes

Both Nox and Tox can drive pdm run as their command runner — exported requirements work well for older CI that doesn’t know about PDM.

CI Integration

# .github/workflows/test.yml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python: ["3.10", "3.11", "3.12"]
    steps:
      - uses: actions/checkout@v4
      - uses: pdm-project/setup-pdm@v4
        with:
          python-version: ${{ matrix.python }}
          cache: true
      - run: pdm install -d
      - run: pdm run test
      - run: pdm run lint

setup-pdm action handles installation, caching, and Python selection. The cache: true option caches __pypackages__ or .venv based on your config.

Migrating from requirements.txt or Poetry

# From requirements.txt
pdm import -f requirements requirements.txt

# From Poetry (pyproject.toml [tool.poetry] section)
pdm import -f poetry pyproject.toml

# Or auto-detect format
pdm import

Then run pdm install to verify everything resolves.

Pre-commit and Hatch Integration

For pre-commit hooks driven from a PDM project, install pre-commit into the PDM env and invoke it via pdm run pre-commit run --all-files from CI. If you compare PDM’s distribution model to Hatch’s, the practical difference is that PDM treats distribution = true as opt-in and ships its own pdm-backend, while Hatch’s hatchling is the PyPA-canonical default — use whichever your team already standardized on rather than mixing build backends across services.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles