How to improve Python packaging
chriswarrick.com
chriswarrick.com
Towards the end of this post there is an interesting observation:
> Discourse, the platform that the discussion was held on, shows the number of times a link was clicked. Granted, this count might not be always accurate, but if we assume it is, the link to the results summary was clicked only 14 times (as of 2023-01-14 21:20 UTC). The discussion has 28 participants and 2.2k views. If we believe the link click counter, half of the discussion participants did not even bother reading what the people think.
Along with that, my concern is that there are too my cooks in the kitchen. A packaging system is never going to cater for more than 90% of use cases, but there will likely be a higher than average representation from people who would sit in the 10%. There is a danger of them never being able to agree on a solution that solves all problems, and then never launch anything.
It would be far better to get something out, have a stake in the ground, and build from there. It also desperately needs to be "Core Python", my understanding is that the PyPA (Python Package Authority) is somewhat disconnected from the core Python development.
Ultimately they need one system that is front and centre, that is the "officially endorsed" tool, and very clear messaging on that.
0: https://discuss.python.org/t/python-packaging-strategy-discu...
- - - -
Someone should do for packaging what pathlib did for file I/O: make a Pythonic model/API for it. Then deployment becomes simple, scriptable, testable, repeatable, etc...
Indeed, most packaging PEPs start out exactly the way you've laid out: a tool or service writes a Pythonic API or model, and it gets standardized so that other tools can rely on it. TFA's problems (which are real ones!) stem mostly from the error before there were serious standardization efforts among Python packaging tools.
I think this is important; otherwise there is a risk behind switching to a new packaging tool and then it being subject to neglect/lack of resources - that makes people averse to switching to it. That is why virtualenv/pip is the lowest common denominator - everyone knows that worst case, those will always continue to work. The "official" tool needs to inspire that sort of confidence.
When I did my every-12-months checkin to see how Python dependency management was going, I sure enough saw that Pipenv is ‘under’ PyPA now. I then learned that a PyPA endorsement or association. doesn’t mean a HUGE amount for the reasons that you and others have already noted. Sure enough, PyPA’s own packaging documentation is hesitant to strongly prefer a front end tool let alone any other part of the stack.
A significantly improved Python dependency management story could very well include Pipenv on the front end for all I care. But not acknowledging the elephant in the room risks repeating past mistakes. Lest we end up with more of the celebrity developer culture we see in communities like JS, but without the seemingly limitless effort and resources.
None of this is to understate the technical challenges, especially taking into account pulling together the current fragmented ecosystem.
And also an under-representation of "average users".
Python the language has always benefitted from its relative simplicity, which I attribute to people like GVR saying "no" to specialized features that accrue on languages like barnacles on a ship (looking at you C++).
With newer languages we see core design teams being much more opinionated about tooling for build and dependendency/packaging management. This is no doubt in response to the dumpster fires of C++ and Python, where "the ecosystem will sort itself out" clearly hasn't worked.
When one starts exploring the standard library, everything that is exposed in meta-programming and runtime internals, or the usual language changes even across minor versions, the picture changes dramatically.
Once upon a time I always devoured the release notes for each release, eventually I lost track of changes.
Not the reason for which the project failed, but the weeks lost trying to replicate results between teams certainly didn't help.
Python is very readable, but I agree that the stdlib is anything but simple.
[1]
$ cat req.txt
requests
git+ssh://git@gitlab.com/foo/bar/amber.git@0.4.0
git+ssh://git@gitlab.com/foo/bar/rebam.git@0.4.0
[2] $ cat buildout.cfg
[buildout]
extends = versions.cfg
extensions = mr.developer
auto-checkout = \*
develop = .
show-picked-versions = true
update-versions-file = versions.cfg
sources-dir = git-sources
parts = py
eggs =
amber
rebam
hammer
[sources]
amber = git git@gitlab.com/foo/bar/amber.git@0.4.0
rebma = git git@gitlab.com/foo/bar/rebma.git@0.4.0
[py]
recipe = zc.recipe.egg
eggs =
${buildout:eggs}
interpreter = py-backend
dependent-scripts = trueI used it for a personal project but gave up a few years ago as some piece of the puzzle seemed broken and abandoned (something hadn't been updated to use a newer version of TLS or something).
I liked buildout though - it was a good system with its bin directory.
The shell script checks if there’s a virtual environment set up with the packages in the requirements.txt installed (it takes a snapshot of the file because virtualenv doesn’t have a DB to query cheaply). Once the environment is set up, it dispatches to running from the virtualenv.
That way when you update a requirements.in file it recompiles it (if the txt is out of date) and installs any new packages, removes packages that shouldn’t be there anymore and updates ones whose version changed (if there’s any changes found). It also lets you trivially run tools with disparate requirements.in without conflicts because each is siloed behind its own virtualenv.
This makes it a trivial experience to use these tools in a shared repo because there’s no worrying about packages / needing to remember to run some command before the right environment is set up. You just modify your code and run it like a regular command-line tool and packages automatically get deployed. It’s also amenable to offline snapshotting / distribution. In fact, I used this to distribute support tooling for factory lines of the Pixel Buds and it worked extremely well.
Plus I really like that the end result is a requirements.txt that can be used with any plain Python+pip environment.
It still has a ton of rough edges like error messages still closer to a stack trace than something actionable and it's slow as hell resolving but it does the job 90 percent of the time without issue.
Also running everything in docker now anyway which side steps some other problems.
We trade security for speed(or "velocity" if you want to be jargon about it).
I just pin everything and go through my projects every couple weeks and bump the deps (unless some really big CVE hits the news).
I know Go has historically done something interesting with selecting the "minimum viable version number" for each dependency, but that's the only relevant idea that comes to mind.
With PyPi, at least it's relatively trivial to self-host a pypi instance and self-manage dependency updates.
In the Python ecosystem (vs e.g. Node), at least the total set of first+second-order dependencies for most normal projects is quite small (e.g. medium double digits to low triple digits). It doesn't feel too painful to manage that magnitude of upgrades on a monthly or quarterly basis.
It seems like the shortest path to a universal package management tool for Python is to add this capability to Poetry, rather than to build something new.
Here's a PyPA project (FD: one I work on) that uses a single pyproject.toml to handle all aspects of packaging (and most non-packaging tool configuration, to boot)[1]. With a single file like that, the only thing you need to do to start a local development environment is:
python -m venv env && . env/bin/activate
python -m pip install .[dev]
(We provide a `make dev` target that does that for you as well.)Similarly, to build distributions, all you need is `build`:
python -m build
[1]: https://github.com/pypa/pip-auditEdit: Unfortunately it's easy to confuse the above tutorial with this one[2], which is specifically for setuptools. So I can appreciate end user confusion around the documentation, particularly in that respect!
[1]: https://packaging.python.org/en/latest/tutorials/packaging-p...
[2]: https://packaging.python.org/en/latest/guides/distributing-p...
To be honest that entire pypa doc should be like two paragraphs long instead of 1000+ words. It should be basically, "ok to package your python app just run <official python tool to init a package>, that's it you're done!". Every decision should be made for me, like it is with npm, cargo, etc. I shouldn't have to think beyond running one command.
That's what python end users need. We don't need a million tools and huge docs with seemingly no vision or goal. We need to get shit done and the less time we faff about with packaging and tooling the better.
The whole 'let the community figure it out' seems to have failed and is causing nothing but more confusion and now attacks on people. The council needs to step up and say, "this is how python packaging will work, period. End of story, end of debate. There is no more discussion on this, the decision is final. All other python packaging tools now are non-standard and not recommended for use anymore".
That was the one good thing a BDFL model for leadership could achieve, making a hard decision in the face of many strong opinions.
I worked at one company that used buildout back then, which seemed relatively rare, but buildout was more flexible and allowed you to do things like installing system packages or running your own scripts. We used it to pull down config files that weren't checked into the repo but still wanted to be shared, helping setup the local developer environment, and a few other things.
A definitive statement from Python.org as to The Way would go so far.
In the context of `pip-audit`, that makes a little less sense: most of our dependencies are semantically versioned, and we'd rather users receive patches and fixes to our subdependencies automatically, rather than having to wait for us to release a corresponding fix version. Similarly, we expect users to install `pip-audit` into pre-existing virtual environments, meaning that excessive pinning will produce overly conservative dependency conflict errors.
[1]: https://github.com/sigstore/sigstore-python/tree/main/instal...
In other words, it's generally a superset of the resolutions collected by `pip-compile`. This may or may not be what you want, or what your users expect!
(The point about other development tooling is, I believe, still accurate -- if you e.g. have `black` installed, `pip freeze` will show it.)
If you go to Python.org and follow through to the beginners guide [0] this is what's suggested:
> There are several methods to install additional Python packages:
> Packages can be installed via the standard Python distutils mode (python setup.py install).
> Many packages can also be installed via the setuptools extension or pip wrapper, see https://pip.pypa.io/.
That is so out of date and fundamentally confuses people coming to Python for the first time. How is pip secondary, and no mention of venv?
The PyPA need to get buy in from Python Core to put one tool front and centre first. It needs to be like Rust with Cargo, literally the first thing you learn to use and core to all beginners guides.
That's not to diminish the work of PyPA, you are all amazing I just want your work to be more obvious!
0: https://docs.python.org/3/using/mac.html#installing-addition...
https://packaging.python.org/en/latest/tutorials/installing-...
https://packaging.python.org/en/latest/tutorials/packaging-p...
install .[dev]
format/syntax defined ? I was trying to find what was possible and how to make sense of it in setup.cfg toolsI found one mention in the docs but no more.
pip install jupyter[notebook]
this is the same thing, just for . (current directory) and dev variant.> Optional components of a distribution may be specified using the extras field:
identifier_end = letterOrDigit | (('-' | '_' | '.' )* letterOrDigit)
identifier = letterOrDigit identifier_end*
name = identifier
extras_list = identifier (wsp* ',' wsp* identifier)*
extras = '[' wsp* extras_list? wsp* ']'
as well as explaining their behavior, albeit briefly:> Extras union in the dependencies they define with the dependencies of the distribution they are attached to.
The resolution on . is explained by the pip documentation[1]:
> pip looks for packages in a number of places: on PyPI (if not disabled via --no-index), in the local filesystem, and in any additional repositories specified via --find-links or --index-url. There is no ordering in the locations that are searched. Rather they are all checked, and the “best” match for the requirements (in terms of version number - see PEP 440 for details) is selected.
[0]: https://peps.python.org/pep-0508/#grammar
[1]: https://pip.pypa.io/en/stable/cli/pip_install/#finding-packa...
[1]: https://www.pypa.io/ [2]: https://pypi.org/static/images/logo-large.svg/
requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"
Python.org defaults to using hatchling in the docs[1].PyPA has a sample repository using setuptools[2]. This project is linked from Python.org on this page.[3]
Python.org recommends using setuptools under packaging recommendations as well, with no mention of hatchling or flit even on that page.[4]
No wonder so many devs are frustrated about this. I've been writing mostly Python for work and fun the past 11 years and I'm f*cking confused what tools to use. When I start a new side project I don't know whether a tool is going to exist and still work in a few years, because there's 10 tools trying to do the same things and it's not clear what the community is actually getting behind.
[1]: https://packaging.python.org/en/latest/tutorials/packaging-p...
[2]: https://github.com/pypa/sampleproject/blob/main/pyproject.to...
[3]: https://packaging.python.org/en/latest/guides/distributing-p...
[4]: https://packaging.python.org/en/latest/guides/tool-recommend...
Then I point Pycharm to use that as my remote interpreter through docker compose.
Not watertight, but a hundred times better dev experience.
You mean a link to a malformed .deb packages?
It's not difficult to do but it's also the wrong way to do it.
So in practice we're back to shipping whole distributions using Docker (or, like we did, abandon Python as an option for development but keep .deb packages for deployment).
These are language agnostic and can get the job done.
>>
>> These are language agnostic and can get the job done.
> Because I have dozens of colleagues, each with their own os/distribution, developing an application that needs to lock down dot releases of dependencies to the versions running in production
I assume that the production environment isn't running dozens of os versions and distributions. For development, using a VM or container running the same os version and distro that's used in production and using that OS's package format for packaging the software and installing it in the dev environment (on the container or VM) would work. You're testing to see if the software works in the production environment, not someone's preferred os/distribution.
> and update them at the same time
I'm not as familiar with apt, but dnf has a version lock feature that would allow you to lock down the dependencies to specific versions. You could update them and test them during development and update the version lock file to pull down the updated version of the dependency when updating production.
This is something that's typically handled by the package maintainers for each linux distro, rather than the ones who developed the application. Some developers maintain their own public repositories for packages they built and instruct end users to add their repositories to their package manager config, but they typically will also include the source archive (if it's open source) and build instructions for those running distributions they haven't built packages for.
Looking at Virtualbox[1], for example, in addition to Mac and Windows, they built packages for Redhat, Ubuntu, Debian, OpenSUSE, and Fedora, and they provided the sources along with build instructions[2] for those who are running distributions where there's no pre-built package. In fact, the last option is the most flexible one though it requires a bit more work for the end user.
And virtualbox is actually a good exemple of what I'm saying: despite being one of the major software in one of the field where Linux is strong if not dominant.
- they feel obliged to distribute themselves their Linux packages - they have to distribute 1 package for windows, 2 for macOS but 12 for Linux.
But this also brings up the issue of vetting dependencies. If you're pulling in a dependency that pulls in 10s of other dependencies (direct and indirect), it gets difficult to vet them. PyPi and npm have already had issues with malicious packages being uploaded. On the other hand, I haven't really found the large number of dependencies being an issue for python packages available in the OS package repositories, and I'm not aware of any incidents with those repositories unlike PyPi and npm.
Additionally, even if you only rely on a container - still no: other container engines have a lower attack surface.
The flaws of python's packaging system (as well as the GIL and other hangups) emerge from the tradeoffs of inventing a language optimised for reusing and connecting as many disparate system binaries as possible.
Its not surprising that such a server scripting language is tightly coupled with the server environment that it runs in. Docker is just one way to ensure a reproducible server environment across machines.
What do you think it could have done differently?
- Make dependency resolution deterministic by default. Unfortunately the whole ecosystem has to buy into it, but the benefits are huge.
- Stop building castles of sand by making more layers of tooling that have to run in Python (and will therefore run in some random poorly managed Python). The language install can't manage the build system install - if anything the build system install should manage the language install. Adding pip to the language distribution was such a backwards decision that it marks the point where I gave up on Python ever fixing their stuff.
- Ignore Linux system package managers (apt etc.), they have a fundamentally broken model and will infect your language ecosystem with that breakage if you try to cater to them.
If you read through the discussion on the Python forum, one of the counterarguments to the PEP is that "If existing tools can already support this, why do we need a PEP for it?" But presumedly a PEP would help push the ecosystem to accommodate __pypackages__, and to solve the aforementioned problems (like broader editor and IDE support).
For what it's worth (as someone that builds Python tooling full-time): I'm generally a fan of moving to a node_modules-like model.
Part of the problem, I'm guessing, is that the PEP kind of sits in a weird space, because it's not up to the PyPA to "approve" it, or whatever -- PEPs are approved by the Python Steering Council.
Per Brett Cannon (Steering Council Member):
> Either consensus has to be reached (which it hasn’t) and/or someone needs to send this to the SC to make a decision (although it is a weird PEP in that it also impacts packaging, so it isn’t even clear who would get final say).
https://discuss.python.org/t/pep-582-python-local-packages-d...
One page defaulted to giving instructions by default using “hatch”, while another page says the official recommendation is setuptools with a setup.cfg and only dynamic things declared in setup.py. Meanwhile, pyproject.toml support is being pushed elsewhere and in beta support for a lot of setup tools features.
There’s way too many tools and too much confusion around which ones to use and which ones are the best to choose going forward. Why can’t we just be like Rust and have one tool that builds and formats and runs tests and everything else?
Pipenv is crap but it’s somehow gained support of the PyCQA, meanwhile I’m not sure if poetry is even mentioned by the Python docs or CQA but it’s the one I’ve been using and it seems great, but I don’t even know if I’ve made the right choice anymore by using that.
All of this fragmentation just leads to developer confusion, newbies and people who have been using the language 10+ years alike.
The problem is that compiled extensions are not a minor use case for Python, they’re hugely widely used for packages even if users don’t use them that much. Building a package with no dependencies is still not that hard. But there is no good way to distribute the compiled packages you depend on as their own thing - Intel release MKL as a “Python” package for e.g. but it is just the C library [1]. But of course, if you’re a package maintainer, the chance that the package maintainer of your dependency has both uploaded it to a package manager in a language they don’t use and that also being the version you need is pretty slim. So the issue becomes “how do I bundle every single possible dependency I might need inside a Wheel”, because source installs are not something most Python users understand. Conda and Spack try and fix this issue by being general package managers that can distribute packages in any language, allowing you to depend on FFTW or Eigen or SUNDIALS or whatever you need in your C extensions, and in all of the discussion I don’t think any proposal really tackles this gap.
How should I be managing environments for these scripts? Do I install dependencies in shared system python? Should I create a shared venv? Where should I store it? Any tools out there that make this decision for you and manage it?
Just the fact that homebrew occasionally updates my installed Pythons and breaks everything makes me reluctant to use Python as a scripting language at all. It's pretty reliable when I have a folder dedicated to a project (poetry is good for this) but I'm pretty fed up with how brittle it all is.
#!/bin/bash
# myscript.sh
venv/bin/python3 myscript.py
You could also skip the shell script and use aliases in your .bashrc.I'm sure I could come up with solutions that are very robust for my particular machine, but I would like something that allows me to share it as a gist and a teammate could just as easily use it, or I can use it myself on another machine without hassle. In other words, a solution contained within the script iteself and maybe one or two binaries outside that make it possible.
I've actually started using a lot of different CLI tools with Docker, especially when the tool isn't available for my OS.
<name>/
pyproject.toml
<name>.py
The pyproject.toml would look like this (using poetry, but you could use a different tool for this): [tool.poetry]
# NOTE: Set <name> to your project name
name = "<name>"
description = "My Script"
version = "0.0.0"
authors = ["Me <me@example.com>"]
[tool.poetry.dependencies]
python = "^3.11"
requests = "^2.28.2"
[tool.poetry.scripts]
# NOTE: Change <name> to match your package name in [tool.poetry]
<name> = "<name>:main"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Then you'd run `pipx install -e .` and the the executable script will be installed in ~/.local/bin.EDIT: Thinking about this some more, doing `brew install pipx` would keep pipx from breaking on brew upgrades, but I guess your installed scripts would still fail if you upgrade from Python 3.N to 3.N+1.
So the only solution, I think, is to keep around all the versions of Python you use, assuming you don't want to upgrade all your scripts when upgrading Python.
I use pyenv to manage Python versions rather than Homebrew, and this seems to avoid the issues you're having, because the Python version used to install whichever tool via pipx sticks around until I explicitly uninstall it.
If you want this automated use pipx, but it is overkill and loses simplicity.
The reason folks give dire warnings against simplicity is that sysad skills have plummeted in recent years. It’s easy to fix the rare conflict by putting the troublemaker in a venv. Outside of a large work project I never need to.
---
Disclaimer before I continue with my own suggestions: I have made small code contributions to pip and pip-tools, and maintain a Zsh frontend to pip-tools+venv, called zpy.
---
Suggestions:
- each folder of code maps to its own default venv, and may map to more venvs for different Python runtimes
- each folder of code has a requirements.in file with top-level dependencies, and a requirements.txt as a lock file
- you can either add a shebang line for your script which explicitly invokes the venv's Python and link that file into ~/.local/bin/, or instead create an external launcher script in ~/.local/bin/.
---
Here's an example of how that might be done using zpy functions:
$ mkdir simple-scripts
$ cd simple-scripts
$ envin # or in subcommand form: zpy envin
==> creating -> ~/.local/share/venvs/280…/venv :: ~/Code/simple-scripts
$ pipacs httpx
==> appending -> requirements.in :: ~/Code/simple-scripts
httpx
==> compiling requirements.in -> requirements.txt :: ~/Code/simple-scripts
anyio==3.6.2 # via httpcore
certifi==2022.12.7 # via httpcore, httpx
h11==0.14.0 # via httpcore
httpcore==0.16.3 # via httpx
httpx==0.23.3 # via -r requirements.in
idna==3.4 # via anyio, rfc3986
rfc3986==1.5.0 # via httpx
sniffio==1.3.0 # via anyio, httpcore, httpx
==> syncing requirements.txt -> env :: ~/Code/simple-scripts
$ print -rl -- 'from httpx import get' 'print(get("https://ifconfig.co/json").json())' >do_it.py
$ vpyshebang do_it.py # or: zpy vpyshebang do_it.py
$ ln -s $PWD/do_it.py ~/.local/bin/do_it
If a Python runtime update breaks your venvs, you can probably fix things up using zpy's pipup function.Which is a shame because Python is an amazing scripting language, the de facto numerical computing standard, and probably a better bash for most bash use cases.
pip install -r requirements.txt -t .lib
and I have import sys
import os
sys.path.append(f"{os.getcwd()}/.lib")
in the top of my script. Some will tell me this is silly, but it just works. Rememer to at .lib to your .gitignore. Else you'll have lot of fun.Then, more recently, I had to run a different project that lacked any documentation as to how I was to run it, had a setup.py file, a Pipfile, and more in it. In trying to get this to run, I managed to make a real pigs ear such that (no doubt thanks to my lack of experience with those tools) I eventually had to delete all my virtual environments, as none of them worked anymore…
So yes, I am 100% in the “one tool to rule them all” camp these days - and although PDM does look promising, right now it isn’t offering me anything above Poetry that I care that strongly about.
As Cato might say, PyPA delenda est.
Not to mention pnpm. Or Bun. Or even Deno. All of which deal with packages, and make different choices.
This isn't to be a pedant, but I think it's a reasonable example of why it's less important that there's a single way to do a thing, or even a single tool vs. multiple alternatives; instead, what I care about is that I don't have to string together tens of tools to do that thing, and that there's some degree of compatibility and standardization in the API between those different alternatives.
[0]: https://nuitka.net
Even this book doesn't cover all options in each area, and it skips almost wholly over conda because I have no personal experience using it. conda and the work in the scientific community adds complexity both to the creation and the consumption side of packaging, and that's one area I'm not sure this post covers all the nuance of when considering how a "one size fits all" solution might work in practice.
I’m not trying to imply you’re one of those people, but this may provide some insight why “Cargo for Python” is not a more widely adopted workflow.
On topic, "Cargo for Python" should be official, singular (no competing standards), opinionated and fast (tm).
There's nothing special about Python that makes it worth enduring this pain. We have other languages with a better developer UX.
Bear in mind this was not written for an audience, let alone a HN audience.
...and having read it, the only thing I'd yell is that "I AGREE WITH ALL YOUR PAIN POINTS". Especially around documentation and package management.
I do like type annotations for improving the reading experience of Python codebases, even if you're not using a type checker, but trying to get people to use them consistently for new code in an org that has a large pre-existing codebase involves either a lot of carrots, or some CI based sticks, and I don't quite have the organisational influence to just drop that on people... ...yet.
The most common and enthusiastic answer was: Packaging and distribution.
I think to say that "large numbers of python developers think packaging is bad and should be fixed" undersells the current sentiment. I think there are large numbers of python developers with boots on the ground who are _utterly sick to death_ of the problem and willing to accept drastic measures resulting inconsiderable suffering if just means it means the issue goes away. People are in other words, getting rather desperate and discontent.
Upstream needs to do a little soul searching and come to an understanding that whatever ideological rock they're chained to that is preventing some solution from emerging has a high chance of not being moored in the needs of reality.
- Dependencies and executables can run arbitrary code during build. For example, Cargo knows almost nothing about how to build C code, and the common workflow is to pull in the popular `cc` library for this and call it in your build.rs.
- There's mostly no such thing as "installing a library", and each project builds its dependencies from source. This is baked in at the Cargo level (the install command just doesn't work for library crates) and also at the language level (no stable ABI besides "extern C").
- Related to that, the final product of a Cargo workflow is a mostly-statically-linked binary, so there isn't really an equivalent to virtualenv.
Python packaging involves arbitrary code execution, even today. While it’s less obvious than it was with `setup.py`, if you’re installing from source, the package can specify any build backend it wants, and that build backend can do anything. This could be mitigated by having an allowlist of build backends, but who would be responsible for vetting the tools, and how would it be guaranteed that an allowed tool is not taken over by someone hostile, and that an allowed tool does not have a system to run arbitrary package-provided code?
Likewise WAR's make it dead simple to add/remove/update applications during runtime.
Of course there are always special cases, but packaging and distribution in the Java world have always been painless to me.
Part of this is due to erlang solving a lot of the problems upfront (releases) and then the Hex team handling a lot of the rest.
But in general, the "Yehuda Katz" lineage (Bundler, Cargo, Yarn v1, mix as José used to be mentored by Yehuda) have pretty good tools worth copying. At least as a base.
Let's say there are two tools, one called pyup and one called pygo.
pyup: responsible for which versions of python are available on your machine, as well
as which one is the default "system" python. It also keeps itself and pygo updated.
pygo: responsible for (among other things)
- allowing a user to specify a python version for a project
- allowing a user to specify a python version for a script
- helping the user install a compatible python version for the project with pyup
- helping the user install a compatible python version for a script with pyup
- selecting a compatible python version/env for project
- selection a compatible python version/env for a script
- allowing a user to specify project dependencies
- allowing a user to specify script dependencies within a script
- determining which dependencies are required for a project
- determining which dependencies are required for a script
- installing project dependencies
- installing script dependencies
- installing projects or scripts as executables
I MUST NOT need a base python install to make any of this work. Tools are constantly mucking with system python and which one is on my path so I can't trust system python, period. pyup and pygo should be their own binaries which invoke python. example.py: #!/usr/bin/env pygo
# version: ~3.11
# requirements:
# requests~=2
import requests
requests.get("https://example.com")
When I run ./example.py for the first time: - pygo helps me install python 3.11
- pygo installs requests in a location that I don't have to worry about
When I run ./example.py for the second time, the script runs without error.If I still need to use something like virtualenv, poetry, or conda on top of this, the unification project has failed.
The interface around `go mod` is kind of confusing, but I have actual trust in the dependency graph it generates for me. Cargo has, afaict, nailed both the interface and trust in what's going on under-the-hood.
In the Python world, Poetry isn't too bad. It's terribly slow in comparison to `go mod` or Cargo, but I generally trust and understand what's happening on the inside, and it's interface is fairly legible to newcomers.
For a single example, all packages in repositories are namespaced, and namespace ownership is verified. [0]
So there's no chance of typosquatting an existing package, nor do you have to worry about someone jumping in and claiming all the "good" names.
[0]: https://central.sonatype.org/publish/#individual-projects-op...
Is quite damning on how much of a better experience Node/JS/TS NPM is than Python's clusterfuck. Working with any other JS/TS open source is easy to get running. Even if you use Poetry in Python, you will run into everyone else using pip-tools, pipenv, pdm, conda, etc.
I always just force my user packages to work. I usually only use numpy/scipy/matplotlib and occasionally a few others so its not that hard, but some sort of npm like experience would be welcome. I know that many are struggling with these environments.
Mostly I just want to see venv eliminated. I really don't like the workflow it requires.
Dealing with all this is why I chose to use golang for writing a CLI utility (even though I'm not a big fan of its error handling boilerplate); static typing + produces a single binary that can be run without needing any of the environment setup. I am aware of various Python tools that can produce a binary too but I think they have their own edge cases and it is just nicer when that can be done out of the box without any dependencies.
> You can also find deficiencies in the tools for the other languages mentioned. Some people think Maven is terrible because it uses XML and Gradle is the way to go, and others think Gradle’s use of a Groovy-based DSL makes things much harder than they need to be and prefer Maven instead.
Yeah but I have never had the installer crap out like pip or its ilk do when installing a package with maven. At worst, it can't find the package if the repositories are not configured properly.
One missing detail is the ability of PDM to support different build backends, which allows for some interesting capabilities: for example, using hatchling as the backend it is possible to utilise hatch's support for dynamic versioning, whcih does not exist in PDM proper. I haven't tried, but wouldn't be surprised if that could allow PDM to support C-extensions by using setuptools as the backend...
> It is also notable that PEP 20, the Zen of Python, states this:
> There should be one-- and preferably only one --obvious way to do it.
> Python packaging definitely does not follow it. There are 14 ways, and none of them is obvious or the only good one. All in all, this is an unsalvageable mess. Why can’t Python pick one tool?
So a few comments on that:
1. There is a reason PEP 20 is called "Zen of Python" and not "Zen of Python Packaging".
1a. Even if it applied to the ecosystem and not just the language itself - PEP 20 is a guide, not a set of divine laws.
2. There is one obvious way, right there in the Python documentation. [0] Yes, it lists several different tools, but "a way to do it" and "a tool to do it with" are two very different topics.
3. "Why can’t Python pick one tool?" I never understood this fixation on silver bullets and "one tool to rule them all"... As long as common principles are well defined - which has been true for Python for quite some time even before PEP 517, with things like PyPI and pip - what is the harm in having multiple competing solutions?
[0] https://packaging.python.org/en/latest/tutorials/packaging-p...
Sometimes I wonder if developers are just making new tools out of self-interest and they want their name on some well-known project, and that's why they don't just work with one of the existing projects to implement their ideas and move the overall community towards a common working model.
> what is the harm in having multiple competing solutions?
Developer confusion and community fragmentation.
To fix problems in Python's world, Python's community should simplify the tools and cultivate a habit to declare the dependency first(maybe this should be mandatory).
> Let’s try removing is-odd to demonstrate how badly designed this package is:
You literally just deleted is-even 's dependency on is-odd then have the audacity to be shocked that it broke?
There's a lot of hatred for the small package philosophy of node.js, but it's also a huge win, stands a good chance of being why javascript has been such a winner, gotten so far: very explicit small things that say what they do on the tin. Rather than repeat yourself by making a copy pasted is-even and then maintaining both, it makes perfect sense to compose functionality, to build off what we have. And it is easier to understand the scope of what a package is & what it can do when it is explicitly limited in scope.
This is another place where there is lots of loud vociferous animosity against what is, but it's there for good reason. And with rare rare exception- left-pad rage-quit deliberate breakage, for example, it serves well. With the exception that yes, inventorying your stuff is hard.
Back when I used it, I really appreciated how little bloat Hapi added to node modules, compared with webpack for example.
Obviously there's a world of difference in what problems the two solve, but still...
Also, having to comb through a bunch of node packages of dubious quality to find some sort of standard approach happens way too often. Like take python requests vs axios.
1. Declarative package manifests. Python's ecosystem is still a mess of various approaches, and the fact that you have to run the setup.py script to determine dependencies is a nightmare. Because of this, running dependency resolution and installing is an order of magnitude faster in Node than in Python.
2. Drop-dead simple isolated environments: everything's in `node_modules`. You literally can't make a mistake by blindly running `npm install` in the project dir. With Python it's on you to manage your virtualenv, which boils down to PATH manipulation and symlinks that you'll have to remember to undo when switching around. There's no default for what to call your venv either, so it's on you to settle on a standard and gitignore it. Every time you run `pip install`, you have to hesitate and make sure you're in the right environment, or else risk borking the wrong env (or your global!)
3. Out-of-the-box comprehensive lockfile support. Debugging Python dependency issues is a nightmare. There's literally no way to figure out why a dependency was installed without using 3rd party tools (like pipdeptree). In Node, simply running `npm install` will automatically generate a proper lockfile.
I work full stack, and the difference is like night and day. I barely think about dependency management in Node.
Give Poetry [1] a shot, it has all the things you've listed here. Just as Node.js has come a long way in the last 5 years, Python has, too. Albeit, in a fashion that was much less centralized, arguably to Python's detriment.
[1]: https://python-poetry.org/history/#100---2019-12-12, released 1.0 in 2019/12.
Poetry can be painfully slow if you happen to depend on many packages that only use `setup.py` to define dependencies.
[1] https://python-poetry.org/docs/faq/#why-is-the-dependency-re...
RangeError: is-odd expects an integer. at isOdd (/tmp/mynodeproject/node_modules/is-odd/index.js:17:11) at isEven (/tmp/mynodeproject/node_modules/is-even/index.js:13:11) at Object.<anonymous> (/tmp/mynodeproject/index.js:3:13)
(But the main point of the demonstration was to showcase dependency resolution and where it looks for packages.)
Is it too small? What if latter the language evolves BigInt? Donwe suffer a patchwork of libraries which have & havent upgraded, sussing around each time to find out?
I think the key thing to recognize is that this is all opinion. Many people dont like the availability of many opions, the ease at which dependencies have grown. And that's fine, there's some real pain here to having ballooning package trees. There's a pevel of conceit though that I feel that often arises, where we mock & shiv packages like is-even . But to me, it's not absolute, it's a matter of taste & preference. It looks weird to outsiders, but it has been enormously powerful & helpful, has been such a key successful element of JS that npm arose & made package management easy & package publishing easy & that we begat a new behavior of capturing all the little helpful things we do & making them available.
Maybe there are good reasons for inlining simple things, but it's not clear to me what the gains really are, or what's wrong with is-even.
It's not easy, it's not tame, but it's unclear what negativity beyond mild inconvenience has been generated. And much of the harm can be diffused with more sensible protection, not simply giving all modules access to everything. Systems like WASI are finally engineering inbuilt protection to sensibly de-risk imports; this is a fault of runtimes for not offering us protection, not our burgeoning package ecosystems for having value, growing.
It's still unclear what the worthwhile protests are.
Saved. Thanks for sharing!
Put another way, my suggestion was to migrate from symbolic links to hard links.
I was working to debug a very weird error that was happening in my code only when my app was raising a warning through my `warnings.py`.
My code was importing numpy, which in turn was trying to import its own `warnings.py`, but because of the python environment precedence, it was loading my own `warnings.py` which unfortunately had the same name.
How on earth was this something that passed the design of the import / package system of python? Why there are not unique id’s for the imported modules?
TLDR: An imported package took dependency on my own code because of module name clashing and lead to unexpected behavior.
By the way I think there is a convention to handle these issues but it appears numpy isn't using it. If I remember correctly, any module with __ infront will be called {package}.__{module}.