Most "I installed it but the import fails" reports come down to pip belonging to a different Python than the one running the code. pip install uses whichever pip is first on PATH. python -m pip install uses the interpreter you just named. That one habit removes a whole family of confusion, and python -m pip --version prints which interpreter it's attached to.
The second habit is one virtual environment per project, made with python -m venv .venv. Activation is a convenience that mostly edits PATH. You can skip it and call .venv/bin/python directly on Linux and macOS, which is also what scripts and editors should do.
What a venv doesn't give you is a reproducible install. It isolates packages from the system, but a requirements file with loose versions still resolves differently next month. For that you need pinned versions with hashes, or something like Nix that pins the interpreter as well. I go back and forth on whether the extra machinery is worth it for small projects. Mostly I just pin.