Getting started with pipenv

Hey again!

My debut python blog & I am super excited to present this to you all. I was never really a Pythonista (I am still not). I have grown to like Python - even over languages that I was once comfortable with. My exposure to Python was primarily during my internship at Setu. Since the past 1-1.5 years, my exposure has only gone upwards & I have used Python for numerous personal & professional projects. I can safely say that Python is my go-to language now for pretty much everything. The rationale is pretty simple - it just works everywhere. My profile requires me to automate a few things now and then. Honestly, Python has been my best bud for these tricks. As a result, I used tools to improve my workflow. This blog is about me sharing one of these experiences. Enough intro, this blog is my experiment with the Python package pipenv & my experience working with it. Spoiler alert: you might end up using this for your projects after this xD. Disclaimer, the first 2 sections of the blog are for beginners.So, if you're an Intermediate python developer who is familiar with pip, virtual environments etc, feel free to skip to the pipenv section! You would be able to appreciate the blog more since this is more of an insight than a tutorial.
Another disclaimer, I use the words dependency and package interchangeably. Please do not get confused - they mean the same thing.
Lets' dive into it!

What is pip?

Even if you’re remotely aware of Python, you would know what pip is. pip is the package manager for Python. It is a command-line tool that installs packages (third-party packages included) by default from PyPi, Python’s official package repository. For understanding, pip is to Python what npm is to Javascript or pub is for Dart/Flutter - not entirely the same though. There have been other tools as well but, pip is the one that has stuck along & is also a part of the official Python installation.

By default, any packages installed via pip get stored in the site-packages directory where your interpreter is installed (python’s interpreter. Subsequently, this implies that, by default, you can find the packages installed on your machine on a path similar to the following:

Copy
/Users/AM/.pyenv/versions/3.8.12/lib/python3.8/site-packages

This will be unique to your system. If you explore this location on your terminal, you can see the list of packages installed on your machine. Here is my list for reference: site-packages Folder
The list is lengthy, but you can see that the path is the same as the path given above. You can also recognize some packages like Crypto, PyInquirer etc. If we want to install a new package, say requests, run:

Copy
pip install requests

And after this, if we inspect the path again, we will find the new package installed. (In my case it was already installed :P)

The above image shows the default behavior. Each pip install, by default goes to the site-packages folder. If you are working on multiple projects - which is likely - then this approach is troublesome. Each project has it's own dependency tree. Let's say you have a project that requires colorama 0.2.2 and there is also a project that requires colorama version 0.4.4. Both versions cannot be installed since both will be stored at site-packages. The existing package version will be overwritten by every new install, hence it would not work.

Virtual Environments?

Virtual environment, virtualenv or venv, is exactly like it sounds. Instead of using the defaults, venv isolates the working directory from the rest of the system. To use virtualenv, we first run:

Copy
pip install virtualenv python3 -m venv env

The first command would install the package to our default location. The second command is creating the virtual environment with the name ‘env’. You can try this yourself by creatina a test directory and running the above commands in it. If you run this command:

Copy
tree -d

(NOTE: tree command works only on Linux by default. Please use a package manager like brew or choco for Windows to install tree if you feel like it. The purpose of this command is to show a tree-like structure of the directory you're in)

This is what it looks like in my case: tree pip

The contents of each folder are as follows:

  1. bin: files that interact with the virtual environment
  2. include: C headers that compile the Python packages
  3. lib: a copy of the Python version along with a site-packages folder where each dependency is installed

For activating the environment, use the following command:

Copy
source env/bin/activate

Once that is done, you can see the environment name on the terminal like so.

tree pip
To check that the environment is indeed independent, use the following code:

Copy
import requests URL = "https://jsonplaceholder.typicode.com/posts/1" def main(): response = requests.get(URL) print(response.text) if __name__ == "__main__": main()

And run this file. The result would be similar to this:

venv vs no venv

In the first run, we see that the response was successful. Our default site-packages location contains requests package already. Hence there were no errors. If we activate the venv & try this again, we get an error. This is because requests does not exist in this virtual environment. You can verify this by checking out the lib folder. The path env/lib/python3.8/site-packages (in my case), does not contain requests. venv also uses a different python interpreter & not the default one on your system. To check this, run the following, one with the virtual environment activated and the other without:

Copy
which python

This would work on Linux and Mac. If you’re using windows (please switch soon 😛), run the following: As you can see, the command gives different paths, implying that the interpreter is unique.

which python

The "pythonic" workflow

Ideally, any python project would include pip and venv. pip would manage the project dependencies, while venv ensures that the project operates in isolation from the rest of the system. If you have worked with Python you'll relate to this statement: we create a requirements.txt file for every project to keep track of its dependencies & their working versions for that project. To do so, we might do something like :

Copy
pip freeze > requirements.txt

The above command would give us a text file that contains all our project dependencies as shown:

Copy
certifi==2021.10.8 charset-normalizer==2.0.10 idna==3.3 requests==2.27.1 urllib3==1.26.8

There are a few problems with this approach:

  1. First & foremost, there is no separation between dev and production packages, which is present in other languages like Javascript (package.json). For example, testing libraries are a dev dependency and therefore must be a part of only dev packages. The library should not be a part of production packages.

  2. Child dependencies (like certifi) are crucial to parent dependencies (like requests), without which the final build would not be deterministic. For instance, certifi releases an update. Our next installation for the package requests would include something different from our initial installation. In a dev and production environment, this would mean that our prod runs on a different config than dev. Consequently, the build might work for us but not for others.

  3. To tackle the 2nd point, we have pip freeze. For each new installation, we can do so and update our requirements, but as a result, we are now responsible for managing the packages. Consequently, this can lead to manual errors later down the line.

  4. Having one requirements.txt file now limits you to a single environment. You cannot have two different lists of packages on the same file (obviously). You can, however, create two files - one for dev dependencies and the other for prod. Now this amplifies the 3rd point. You are now manually managing packages for two environments!

These issues are not apparent when the project size is small & moving parts are less. But in organizations where the team size is considerable & some environments have a very specific dependency tree, it is crucial to tackle the above-listed problems. As you might have guessed, pipenv is here to save the day!

pipenv to the rescue!

pipenv is a combination of pip and virtualenv. It uses both tools under the hood to provide us with the necessary functionality, in a single package. Let’s start using it. I urge you to test the following things that I have presented yourself - along with the blog, if possible. Execute the following command to install pipenv:

Copy
pip install pipenv

As bizarre as it might be, we are using pip to install this package! But I can assure you - this is a one-time setup. We need to let this package install in the site-packages folder. We can use this whenever we want as a regular command-line utility.
Let’s create a test directory to try this out:

Copy
mkdir test_pipenv && cd test_pipenv

Within this directory, type the following:

Copy
pipenv shell

If you have a single python version installed the above command would work. If however you have multiple python version, it is better to specify which version will be used. This would ensure that **pipenv check **command runs properly & can detect vulnerabilities. For instance, the command I should run:

Copy
pipenv shell --python 3.8.12

While discussing package version conflicts later in this blog, I will use the above command. For now, we'll proceed with the defaults.

See the logs that are generated for this (I have given logs from my machine for reference).

pipenv Logs

Please ignore the warnings, pipenv works regardless.

This command is creating a virtualenv for this project since it's unable to find an existing environment. If it had found one, it would have activated it instead of creating a new one like this. It is downloading some packages which are necessary for package management, namely wheel, setuptool, etc. Then it creates the new virtual environment and gives us the location for it. This location is OS-specific. As seen from the image, mine is: /Users/AM/.local/share/virtualenvs/pipenv_test--y0hFAjT If you notice, it contains the **project name - hash**. This is how pipenv knows which environment to activate depending on the project. The reason for this is that pipenv stores virtual environment information centrally. You can justify since there is no new folder created for this virtual environment. Next, a Pipfile is generated. For those who have worked on npm projects, treat this as your package.json. Finally, it just activates the environment, as discussed.

One more thing, you cannot use deactivate to step out of the virtual environment. Instead use exit to close the virtual environment in case of pipenv. If you deactivate, it would still remain active. This is the thread for reference: StackOverflow Discussion

If you ls right now, you would see a Pipfile in the location. Let’s check its contents.


Pipfile-Raw

Let’s go from bottom to top.

The last block states the Python version used in the project. This would highlight the version that is used for the production build. It would also give a warning when this version is not met for a particular project (during the pipenv install command). Then there are the dev-packages and packages. The separation of both is what was the limitation with pip. Both are managed by pipenv hence our responsibility reduces. Up top, we have the source. Whenever we do a fresh install, this source would specify the place pipenv would look in for our dependency. Fun fact, this file is a TOML (Tom's Obvious, Minimal Language) file. TOML files are standard in the python package development process.

Let’s install requests as we did in the previous example. To do so, we need to update the command to this:

Copy
pipenv install requests

Instead of pip, pipenv will handle our packages now. Just a reminder, we are already in a virtual environment. Subsequently, new dependencies will be installed within our virtual env. The logs for the above command are below:


pipenv install requests

Ignore the warnings again. I have multiple python versions installed using pyenv, hence the issue. You might not see them at all. The command first adds the dependency, then checks for a Pipfile.lock file. When I said Pipfile was package.json I meant it in this sense. Every npm project has a .lock file that contains something like this:

Copy
{ "requires": true, "lockfileVersion": 1, "dependencies": { "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3. 0.0.tgz", "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" }, "cowsay": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/cowsay/-/cowsay-1.3.1.tgz" , "integrity": "sha512-3PVFe6FePVtPj1HTeLin9v8WyLl+VmM1l1H/5P+BTTDkM Ajufp+0F9eLjzRnOHzVAYeIYFF5po5NjRrgefnRMQ==", "requires": { "get-stdin": "^5.0.1", "optimist": "~0.6.1", "string-width": "~2.1.1", "strip-eof": "^1.0.0" } }, "get-stdin": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0. 1.tgz", "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=" }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/ is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "minimist": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10 .tgz", "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" }, "optimist": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", "requires": { "minimist": "~0.0.1", "wordwrap": "~0.0.2" } }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "requires": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" } }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "requires": { "ansi-regex": "^3.0.0" } }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, "wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" } } }

When we check the contents of the Pipfile.lock we get something like this:

Copy
{ "_meta": { "hash": { "sha256": "acbc8c4e7f2f98f1059b2a93d581ef43f4aa0c9741e64e6253adff8e35fbd99e" }, "pipfile-spec": 6, "requires": { "python_version": "3.8" }, "sources": [ { "name": "pypi", "url": "https://pypi.org/simple", "verify_ssl": true } ] }, "default": { "certifi": { "hashes": [ "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" ], "version": "==2021.10.8" }, "charset-normalizer": { "hashes": [ "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd", "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455" ], "markers": "python_version >= '3'", "version": "==2.0.10" }, "idna": { "hashes": [ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], "markers": "python_version >= '3'", "version": "==3.3" }, "requests": { "hashes": [ "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" ], "index": "pypi", "version": "==2.27.1" }, "urllib3": { "hashes": [ "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.26.8" } }, "develop": {} }

Pipfile.lock is also a JSON file. It contains the following sections:

  1. First part defines some meta information & specifies the spec being used for the Pipfile. It also specifies the Python version.
  2. Next section states where the libraries/packages were sources from.
  3. The default section contains each of the packages - that is both parent and child packages - that were installed in the virtual environment. Each library has a name, a hash describing its content and is fully pinned to the version that is installed in this case.
  4. Final section is about the dev dependencies. If you install something with the --dev flag, once installed, the package would be shown here.

Pipfile.lock is locking dev-packages and packages. Locking means that versions for installed packages are being pinned to one specific version. The install command also adds the parent package to the Pipfile.


pipenv with requests

As you can see, the only change is that the package requests has been added to the packages section. There is an ‘*’ in front of the package name, which means that any version is allowed. From PyPi this would fetch the most recent version. Had we specified the version during installation, that version number would be here. pipenv does not notify the user about child dependencies. I find this feature useful since it keeps the dependency tree lean. Only the main packages are all we should care about.

Let’s say later down the line you would want to update the packages used or, more likely, pin the packages to a specific version. You can do it in the same way you would have done it with pip. Just uninstall the existing package and re-install using pipenv. The version in the Pipfile would be updated automatically & by default it would also generate a new lock file to pin the new version being used in the project. Simply editing the Pipfile and then running pipenv install would not work. This is because the lock file has already pinned the version being used, therefore the version in the lock file is final. If you want to update the current version to a new one, uninstall first & re-install. This check is amazing in my opinion, if the lock file exists your package version would always be pinned. However, if the lock file does not exist, package numbers from the Pipfile will be used. So another way to tackle the lock is to delete the lock file (not recommended) and start over with

Copy
pipenv install

To install dev packages- in general or in the above case - simply add the --dev keyword to the install command. Dev packages are the ones that are not required in a production environment. Once installed, either as dev or normal package, that package is avaiable for us in your virtual environment. Hence you do no need to double install or maintain two separate lists. NOTE: the above command installs packages from Pipfile. If however you would want to install packages that mimic the production environment, use the lock file. Command for this is given in the next section.

Lock files are strict and do no handle conflicts well. If you have packages that have conflicting child dependencies, pipenv would notify you during the installation (Personal experience). This is a great thing because, in the case of pip, if an older version of a dependency was installed, pip would update or downgrade it according to the most recent install command. pipenv does not do that. Since versions need to be locked in the Pipfile.lock, pipenv makes sure that no conflicts like these exist. However, you can use an override called --skip-lock. This is not a recommended approach & should only be done if you know what you’re doing. --skip-lock would not generate a new lock file, therefore versions would not be pinned. Any update to the Pipfile would result in an update to the existing package - requirements.txt behaviour again.

skip-lock

Useful commands in pipenv

pipenv includes numerous commands that can be used to enhance your workflow. Given below is a list of such commands:

Copy
pipenv install --ignore-pipfile

When you would want your development environment to mimic your production environment, instead of using the Pipfile, use the Pipfile.lock. And corollary to this, use Pipfile.lock for all your production builds.

Copy
pipenv install --dev

This command installs both packages and dev packages. In the case of a normal pipenv install, dev packages are ignored. This is useful for new developers that are setting up the project for the first time.

Copy
pipenv graph

This command would show the tree-like structure of the project packages. This is what it would look like:

pipenv graph

The parent packages are shown at the top, and their child dependencies are shown below it. This is useful in evaluating package dependencies & can help resolve conflicts, if they occur.

Copy
pipenv graph --reverse

The command will present the dependencies in a tree-like structure but reverse. Child dependencies are presented on top and parent dependencies are given below them.

Copy
pipenv run <insert command here>

For running a command in the virtual environment without launching a shell. This is similar to docker exec -it. I don't use it often but it might be useful in case each project has it's own python version and some packages are bound to a particular version only. No examples for this use case since I have not encountered this myself.

Copy
pipenv check

This checks for security vulnerabilities in the environment. npm audit is the functional equivalent for this.

Copy
pipenv --venv

Gives the path for the virtual environment of the project.

Copy
pipenv --where

The command finds out and presents the projects' home

Copy
pipenv uninstall --all

Uninstall all packages. --all-dev is used to delete just the dev packages

Copy
pipenv open flask

This command opens the given package for editing. I don't use this alot since the default package behaviour is good enough, but if you would want to tweak something in a pacakge, this is the command for it.

Conclusion

My verdict is based purely on personal experience. I believe that pipenv is an extremely helpful tool, capable of revolutionizing the python workflow. I would encourage you to use pipenv for your upcoming python projects. Honestly, for smaller projects with fewer collaborators, the default workflow is more than enough. For projects with multiple collaborators & which are large scale, pipenv is definitely a boon. I personally use the Pipfile while developing the project. Once I'm done with it, I generate a lock file and deploy it using the lock file. This has helped me achieve clarity with my packages. Again, this is personal opinion & therefore can be wrong. For more seasoned developers - who are switching to python from languages like JS or who would want to have a more deterministic workflow - I would highly recommend pipenv. For the curious ones among us, this is the pipenv official documentation:

Pipenv Official

Do check it out! I hope you found this valuable! Thanks again and I'll see you in the next one!

© 2019-2022 • Copied with ❤️ by Aayush Malhotra