A few years back, I started using poetry to manage dependencies for all my Python projects. I ran into some minor issues early on but haven’t had any problems recently and prefer it to any of the other dependency management / packaging solutions I’ve tried so far.

Recently, I’ve started hearing about pdm and how it’s the bee’s knees. I did a search for “pdm vs poetry” and didn’t find much, so I thought I’d play around with pdm a bit and write something myself.

Creating a new project

Both tools have an init command. Create a new directory, cd into it, run <tool> init, and follow the prompts.

One difference here is that poetry will prompt for more package metadata and allow you to define your dependencies at this stage.

The pyproject.toml generated by pdm is pretty bare, so you’ll have to fill in a bunch of stuff like the project name, version, etc.

pdm will also ask if you want to create a virtualenv at this stage. If you choose y, it will create a virtualenv in ./venv. If you choose n, it will use PEP 582 mode (more on PEP 582 below).

Converting an existing project from poetry to pdm

pdm has an import command that you can run on an existing pyproject.toml. It will keep all of your existing config, except it will overwrite the [build-system] section.

If you’re using the [tool.poetry.group.dev.dependencies] section to define your dev dependencies, pdm won’t recognize it, so I’d advise changing it back to [tool.poetry.dev-dependencies] to avoid issues with that.

pdm also didn’t import local develop dependencies correctly. It converted poetry dependencies like this:

some-package = { path = "../some-local-package", develop = true }

to:

[tool.poetry.dev-dependencies]
dev = [
    "some-local-package @ ",
]

This causes pdm install to throw a cryptic TypeError. To fix it, you have to remove the @ and change the spec to:

"-e file:///abs/path/to/some-local-package"

Differences

  • poetry init prompts for more metadata and creates a cleaner initial pyproject.toml vs pdm init.

  • poetry uses TOML syntax to define dependencies while pdm uses pip-style strings. I prefer poetry’s approach:

    # poetry
    [tool.poetry.dependencies]
    django = ">=4.1.4"
    markdown = ">=3.4.1"
    
    [tool.poetry.group.dev.dependencies]
    some-local-package = { path = "../some-package", develop = true }
    
    # pdm
    [project]
    dependencies = [
        "django>=4.1.4",
        "markdown>=3.4.1",
    ]
    
    [tool.pdm.dev-dependencies]
    dev = [
        "-e file:///abs/path/to/some-local-package",
    
        # NOTE: This works only if you're using specific build backends
        "-e file:///${PROJECT_ROOT}/../some-local-package",
    ]
  • As shown above, poetry lets you use npm-style ^ and ~ dependency constraints or pip-style >= constraints. I prefer the former for applications and the latter for libraries.

  • As shown above, poetry makes it easy to specify relative paths to local dependencies. pdm will let you do something similar using the injected PROJECT_ROOT var, but only if you’re using the correct build backend.

  • poetry has less options with regard to virtualenv vs __pypackages__ and build backends. I’m not sure this is better per se but I found myself scratching my head a bit more with pdm (although I admit this could be due to being more familiar with poetry).

  • The size of the directory where packages are installed is slightly smaller when using pdm. This is because poetry installs pip, setuptools, and wheel into the virtualenv while pdm doesn’t. poetry also seems to pre-compile all Python modules up front while pdm doesn’t.

Dependency resolution

I didn’t notice a huge difference in the speed of dependency resolution or initial installation (pre-cache).

PEP 582

Using PEP 582 mode with pdm requires a bit of setup since Python doesn’t support it out of the box, but it’s not too bad (and isn’t specific to pdm). There’s a PEP 582 page on the pdm site that tells you how to do this.

It’s pretty straightforward, but I did run into one issue when running pdm --pep582 in fish. It outputs some lines that export environment variables using set -x, but I had to change them to set -gx to get everything to work.

The other thing you might want to do is put __pypackages__/3.x/bin in your $PATH when you cd into a project directory. I wrote some fish scripts to do this, but you could just use direnv.

Conclusion

I’ll probably keep an eye on pdm, but poetry seems a bit more polished currently. If you’re already using poetry, I don’t see any reason to switch. If you haven’t gone down this particular rabbit hole yet, I’d probably recommend poetry at this point.

The most interesting thing about pdm to me is the PEP 582 / __pypackages__ support. After playing with it a bit, I do think it would be a good addition to Python.

Comments, Corrections, Suggestions

This blog doesn’t have a comment system, but you can send feedback via the contact form on the home page.

NOTE: I originally published this on 23 December, but there were a couple of errors. This version removes/corrects those errors and does more of an actual comparison.