Hacker News new | past | comments | ask | show | jobs | submit login

I thought this was about loading two incompatible versions of a shared object into the same address space at first :-)

The author correctly contrasts Rust (and NPM's) behavior with that of Python/pip, where only one version per package name is allowed. The Python packaging ecosystem could in theory standardize a form of package name mangling wherein multiple versions could be imported simultaneously (akin to what's currently possible with multiple vendored versions), but that would likely be a significant undertaking given that a lot of applications probably - accidentally - break the indirect relationship and directly import their transitive dependencies.

(The more I work in Python, the more I think that Python's approach is actually a good one: preventing multiple versions of the same package prevents dependency graph spaghetti when every subdependency depends on a slightly different version, and provides a strong incentive to keep public API surfaces small and flexible. But I don't think that was the intention, more of an accidental perk of an otherwise informal approach to packaging.)




> (The more I work in Python, the more I think that Python's approach is actually a good one ...)

I've come to the opposite conclusion. I've "git cloned" several programs in both python and ruby (which has the same behaviour) only to discover that I can't actually install the project's dependencies. The larger your gemfile / requirements.txt is, the more likely this is to happen. All it takes is a couple packages in your tree to update their own dependencies out of sync with one another and you can run into this problem. A build that worked yesterday doesn't work today. Not because anyone made a mistake - but just because you got unlucky. Ugh.

Its a completely unnecessary landmine. Worse yet, new developers (or new teammembers) are very likely to run into this problem as it shows up when you're getting your dev environment setup.

This problem is entirely unnecessary. In (almost) every way, software should treat foo-1.x.x as a totally distinct package from foo-2.x.x. They're mutually incompatible anyway, and semantically the only thing they share is their name. There's no reason both packages can't be loaded into the package namespace at the same time. No reason but the mistakes of shortsighted package management systems.

RAM is cheap. My attention is expensive. Print a warning if you must, and I'll fix it when I feel like it.


I'm not saying this hasn't happened to you, but I'm curious: are you working with scientific Python codebases or similar? I've done Python development off and on for the last ~10 years, and I think I can count the number of times I've had transitive conflicts on a single hand. But I almost never touch scientific/statistical/etc. Python codebases, so I'm curious is this is a discipline/practice concern in different subsets of the ecosystem.

(One of the ways I have seen this happen in this past is people attempting to use multiple requirements sources without synchronizing them or resolving them simultaneously. That's indeed a highway to pain city, and it's why modern Python packaging emphasizes either using a single standard metadata file like pyproject.toml or a fully locked environment specification like a frozen requirements file.)


I've encountered the same problem with Python codebases in the LLM / machine learning space. The requirements.txt files for those projects are full of unversioned dependencies, including Git repositories at some floating ref (such as master/HEAD).

In the easy cases, digging through the PyPI version history to identify the latest version as of some date is enough to get a working install (as far as I can tell -- maybe it's half-broken and I only use the working half?). In the hard cases, it may take an entire day to locate a CI log or contemporary bug report or something that lists out all the installed package versions.

It doesn't help that every Python-based project seems to have its own bespoke packaging system. It's never just pip + requirements.txt, it'll have a Dockerfile with `apt update`, or some weird meta-packaging thing like Conda that adds it own layers of non-determinism. Overall the feeling is that it was only barely holding together on the author's original machine, and getting it to build anywhere else is pure luck.

For example: https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob... (with some discussion at https://github.com/AUTOMATIC1111/stable-diffusion-webui/disc...)


That’s still the happy case. Once upon a time I spent four days chasing dependencies before reaching out to the original author, who admitted that it hasn’t actually worked in several months but he kept on editing code anyway.

The program depended on fundamentally incompatible sub-dependencies, on different major versions.


I've had similar problems with python packaging in both the web dev and embedded spaces. There are ways to largely solve these issues (use package managers with lock files and do irregular dependency updates), but I rarely see that being done in projects I work in.


If you use gRPC directly and some other library in your stack does it as well it's very likely you end up with conflicts either on gRPC itself or the proto library under the hood.


I don't know, I can see it both ways. I think it depends on programming context. On one hand you're right that it's annoying and a technically unnecessary gotcha, but for some Python use cases it simplifies the mental model to simply Know which version of a particular package is running. For example, pandas and numpy are IMO bad offenders for transitive dependency issues, but it's because they're used as building blocks everywhere and are intended to be used compositionally. It's not uncommon to have to step through a data pipeline to debug it. That would become confusing if it's using 5 different major versions of pandas because each package brought it's own. Or a trip down the call stack involves multiple different versions of numpy at each level.

For web dev and something like requests, it's just not as big of a deal to have a bunch of versions installed. You don't typically use/debug that kind of functionality in a way that would cause confusion. That said, it would be definitely be great sometimes to just be like "pip, I don't care, just make it work".


Yeah; you've gotta pick your poison. Either you sometimes end up with multiple copies of numpy installed, or sometimes pip errors out and will randomly, at the worst possible time, refuse to install the dependencies of your project. Having experienced both problems a lot of times, I'll take the former answer every time thankyou. I never want surprise, blocking errors to appear out of nowhere. Especially when its always the new team members who run into them. Thats horrible DX.

Having multiple copies of numpy installed isn't an emergency. Tidy it up at your leisure. Its pretty rare that you end up with multiple copies of the same library installed in your dependency tree anyway.

As for debugging - well, debugging should still work fine so long as your debugging tools treat foo-1.x.x as if it were a completely distinct package from foo-2.x.x. Nodejs and rust both handle this by installing both versions in separate directories. The stack trace names the full file path, and the debugger uses that to find the relevant file. Simple pimple. It works like a charm.

> For web dev and something like requests, it's just not as big of a deal to have a bunch of versions installed.

I think you've got it backwards. Its much more of a big deal on the web because bundle size matters. You don't want to bundle multiple 150kb timezone databases in your website.

I've also worked on multiple web projects which ran into problems from multiple versions of React being installed. Its a common problem to run into, because a lot of naively written web components directly depend on react. Then when the major version of react changes, you can end up with a webpage trying to render a component tree, where some components are created using foreign versions of react. That causes utter chaos. Thankfully, react detects this automatically and it yells at you in the console when it happens.


> Its a completely unnecessary landmine. [...]

This! I have transitive conflicts almost every time I clone an exsting Python repo. It's one of the main reasons why Python == PITA^3 in my head.

Maybe it's the kind of Python repos I clone. Mostly ML/diffusion & computer graphics stuff.

As a Rustacean I had hoped that the Rhye + uv combo would 'fix' this but I now understand they won't.


Another thing I appreciate about this in the Python world is it avoids an issue I've seen in node a lot, which is people being too clever by a half and pre-emptively adding major version bounds to their library. So foo depends on "bar<9", despite bar 9, 10, 11, 12, 13, and 14 all working with foo's usage of bar.

The end result of this is that you end up with some random library in your stack (4 transitive layers deep because of course it is) holding back stuff like chokadir in a huge chunk of your dep tree for... no real good reason. So you now have several copies of a huge library.

Of course new major versions might break your usage! Minor versions might as well! Patch versions too sometimes! Upper bounds pre-emptively set help mainly in one thing, and that's reducing the number of people who would help "beta-test" new major versions because they don't care enough to pin their own dependencies.


> dependency graph spaghetti

The worst spaghetti comes from hard dependencies on minor versions and revisions.

I will die on the hill that you should only ever specify dependencies on “at least this major-minor (and optionally and rarely revision for a bugfix)” in whatever the syntax is for your preferred language. Excepting of course a known incompatibility with a specific version or range of versions, and/or developers who refuse to get on the semver bandwagon who should collectively be rounded up and yelled at.

In Rust, Cargo makes this super easy: “x.y.z” means “>= x.y.z, < (x+1).0.0”.

It’s fine to ship a generated lock file that locks everything to a fixed, known-good version of all your dependencies. But you should be able to trivially run an update that will bring everything to the latest minor and revision (and alert on newer major versions).


There's a subtle point there though. When you rely on something that was introduced in x.y.z, stating that your version requirement is x.y.0 is an error that can easily cause downstream breakage.


I’m confused. If you rely on a feature introduced in X.y.z why would you specify X.y.0 to begin with (and not just X.y.z)?

In practice, usual rust projects that have not put a ton of work into their dependencies encode X.y.z in Cargo.toml matching the current release at the time they developed the system. So you get at worst an unnecessarily higher version requirement but never a lower one.

Moreover, rust semver would normally imply that new features should only be introduced in X.y releases, so this doesn’t really happen in practice!


It's easy to accidentally ship a minimum version requirement that is out of date when you also consistently use lock files pinned to newer versions. The code may silently depend on something introduced in a newer version pulled in by the lock file.


You can have a CI builder using direct-minimal-versions to check this.


Point releases are often bugfix releases, i.e. not api changes but runtime changes. CI won’t help without very specific accompanying tests.


I have literally never run into this being a problem in practice. If someone downstream ever did notice, they can just specify a higher minimum version constraint.


Just have CI build with the minimum and the maximum.


My point was not about "x.y.0" (I mispoke), but "x.y" (or "0.x") that causes this problem.

Take a look at any random crates's cargo config, and you'll regularly see dependencies specified as "1" or "0.3" instead of "1.0.119" or "0.3.39". If another crate depends on this crate, and has a more precise version needed (say "=1.0.100" (perhaps due to a bug introduced in "1.0.101", but the included library relies on features introduced in some version after "1.0.100", then your library won't compile. Stating your dependency on "1" instead of "1.0.119" is what caused this problem.

This is not hypothetical - I've run into this quite a few times with crates that over-specify their dependencies interacting with crates that under-specify theirs.

Yes, the solution to this is pretty simple - check minimal versions in CI. That's something I do for most of my stuff, but it's not universal.


> In Rust, Cargo makes this super easy: “x.y.z” means “>= x.y.z, < (x+1).0.0”.

With the added special case of `0.x.y` meaning `>= 0.x.y, < 0.(x+1).0`, going beyond what semver specifies.


Building against recent and old version and publishing ranged lockfiles should be mandatory.




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: