Some combination of requirements.txt (which lets you dial in, with great precision, each of the libraries you need, and is trivially created in < 50msec with "pip freeze")
1. Containers - That's it. You control everything in it. 2. virtualenv - Every environment comes their own version of python and their own set of packages and versions of those packages. Add virtualenvwrappers and you can create/switch trivially between them.
It's been at least 2 years since I've run into a issue with python and dependencies that wasn't solved by both of those approaches.
It's a far cry from being able to download any git repo and call `cargo build`/`cargo run` or `npm install`/`npm run` with confidence that it's just going to work.
Where I work - life is simple. You build your project in a virtualenv so it only has the libraries it needs, generate a clean requirements.txt, check it into git with a requirements.txt - everyone can run it, and, because we have day-1 onboarding to teach everyone virtualenv/virtualenvwrapper - the first thing a person does before installing the application is mkvirtualenv.
I see a lot of references to Poetry here - but I've never been able to interest any of our senior developers into looking at it - they are pretty happy with our existing system.
It is true that most recent languages ship with these from day 1, but ecosystems rarely lack this kind of stuff. I mean, even my vim has a package manager nowadays.
As for whether you want to salvage old code that isn't provided with package management, it's up to you. But you would have this kind of problem with any old, unmaintained codebase.
The problem is that this leaves us with 2 years of documentation that's reliable and addresses easily-solved problems, and 28 years of everything else that will confuse anyone new to the language. Not ideal when accessibility is one of the language's primary selling points.
One of the major problems with fixing design choices or odd behaviours in software is that all of the old threads and posts don't just disappear, and people are now going to be lead down paths that are not only so convoluted and ridiculous that they were eventually changed, but often paths that don't even work any more.
It's very very tough to fix that problem retroactively.
Pipenv could have been it but it never got far.
===
[Marine has been wounded]
Marine Private: AHHHH my arm, my arm!
Major Payne: Want me to show you a little trick to take your mind off that arm?
[Marine nods and Payne grabs the private's pinky finger]
Major Payne: Now you might feel a little pressure.
[Major Payne breaks the Marine's pinky]
Marine Private: AUGGGGH! My finger, my finger!
Major Payne: Works every time.
====
That's kind of how I feel about Docker. Before, you had a problem. With Docker, you have a new, bigger problem (and most of your old problem hasn't gone away; it's just been masked for a while).
(And yes, I know I'm in the minority here)
* Robust systems shouldn't be tied to pinned versions. If your code works with PostgreSQL 9.6.19, and doesn't work with 9.6.20 or 9.6.18, that's usually the sign of something going very, very wrong.
* In particular, robust systems should always work with the latest versions of libraries. In most cases, they should work with stock versions of libraries too (whatever comes with Ubuntu LTS, Fedora, or similar). It's okay if you have one or two dependencies in a system beyond that, but if it's a messy web, that's a sign of something going very, very wrong.
* Even if that's not happening, as much as I appreciate having decoupled, independent teams, your whole system should work with the same versions of tools and libraries. If one microservice only works with PostgreSQL 11.10, and another with 12.07, that's a sign of something having gone way off the rails.
These aren't hard-and-fast rules -- exceptional circumstances come up (e.g. if you're porting Python 2->Python 3, everything might not land at the same time) -- but these should be rare enough to be individually approved (and usually NOT approved) by your chief architect/architecture council/CTO/however you structure this thing.
For the most part, I've seen Docker act as an enabler of bad practices:
* Each developer can have an identical install, so version dependencies creep in
* Each team has their own container, and it's easy for versions and technologies to diverge
* With per-team setups, you end up with an uncontrollable security perimeter, since you need to apply patches to a half-dozen different versions of the same library (or worse, libraries performing the same function)
The docker/microservices/etc. mode of operating gives a huge short-term productivity boost, but I haven't actually seen a case on teams I've been on where the benefits outweigh the long-term costs. That's not to say they don't exist, but they're in the minority.
For the most part, I use Python virtual environments and similar, but by the time you hit docker, I back away.
Docker alone doesn't solve the problem and neither does pip unless you take extra steps.
Here's a common use case to demonstrate the issue:
I open source a web app written in Flask and push it to GitHub today with a requirements.txt file that only has top level dependencies (such as Flask, SQLAlchemy, etc.) included, all pinned down to their exact patch version.
You come in 3 months from now and clone the project and run docker-compose build.
At this point in time you're going to get different versions than I had 3 months ago for many sub-dependencies. This could result in broken builds. This happened multiple times with Celery and its sub-dependency of Vine and Flask with its sub-dependency of Werkzeug.
So the answer is simple right, just pip freeze your requirements.txt file. That works but now you have 100 dependencies in this file when really only about 8 of them are top level dependencies. It becomes a nightmare to maintain that as a human. You basically need to become a human dependency resolution machine that traces every dependency to each dependency.
Fortunately pip has an answer to this with the -c flag but for such a big problem it's not very well documented or talked about.
It is a solvable problem tho, to have a separate lock file with pip without using any external tools and the solution works with and without Docker. I have an example of it in this Docker Flask example repo https://github.com/nickjj/docker-flask-example#updating-depe..., but it'll work without Docker too.
Also, Docker is not a universal and secure solution. It works great as an "universal server executable format and/or programming environment" on Linux, but less so on Windows, macOS and especially FreeBSD.
Yes at some level it solves your problem, but it adds a lot of complexity which doesn't need to exist. Also now depend on someone in the middle who takes effort to manage, and might not do exactly what you want.
If you are developing an open source software that people can install on their machines, is a terrible solution. In that case you should package it correctly and distribute it via pip, so people can easily install it on their systems.
Python is one of the most "accessible" languages in terms of the actually programming experience, but making a project reproducible is a nightmare. There doesn't seem to be a real "right way" to manage dependencies, and getting a project running seems to often start with figuring out how the author decided to encapsulate or virtualize the environment their project is running in, since changing your system python for one project can break another.
I know it's an older language, so many lessons have been learned, but after working with Rust, or even NPM it seems amazing that developers tolerate this situation.