Publicações

Gerenciamento de dependências com Python e a batalha entre Pipenv e Poetry

Lucas Souto
Lucas Souto Backend Developer
Renan Santana
Renan Santana Backend Developer

Gerenciamento de dependências, em várias linguagens, geralmente é algo bem definido, com ferramentas bem estabelecidas e oficialmente recomendadas para tal propósito. Mas este cenário tende a ser um pouco mais problemático com Python.

Por ser uma linguagem "multiparadigma", onde é possível desenvolver para praticamente todas as áreas da computação, temos um ambiente vasto de como planejar, arquitetar e construir um software. Por mais que muitas outras caracteristicas devem ser levadas em conta, nesse post iremos tratar sobre o Pipenv e o Poetry, que são duas das principais ferramentas para gerenciamento de dependência hoje para Python.

Uma breve introdução sobre gerenciamento de dependência com Python

Há alguns anos, a maneira ideal de gerenciar pacotes dentro de um projeto era através de um arquivo chamado requirements.txt. Nesse arquivo de texto, define-se os pacotes a serem instalados e suas respectivas versões, e através do pip eles são instalados com um único comando: pip install -r requirements.txt. Até hoje essa é uma forma aplicada, pensemos agora seus aspectos positivos e negativos.

O lado bom de utilizar essa abordagem é que você só precisa de um arquivo definindo o que sua aplicação precisa para ser executada, podendo versionar e facilitar para que um time utilize as mesmas lib's e versões.

Mas parando por aí, existem outras diversas lacunas que precisam ser preenchidas quando estamos tratando de um software mais complexo com ambientes de desenvolvimento, homologação, produção, bem como quando precisamos isolar os pacotes que serão instalados do resto do sistema, e também quando precisamos ter versões específicas da linguagem e outros vários pontos não menos importantes.

Até podemos com o pip tentar "driblar" alguns desses problemas acrescentando outras ferramentas. Exemplificando, para o problema de instalar pacotes para ambientes diferentes, podemos utilizar vários arquivos de texto diferentes onde cada um declara o que precisa ser instalado (requirements-dev.txt, requirements-homol.txt, requirements-prod.txt, etc); para separar as dependências e não instalar no root da máquina, podemos utilizar o virtualenv, que cria um ambiente isolado da sua máquina e não conflita com dependências do sistema ou de outros projetos; para melhorar o uso do virtualenv, podemos utilizar o virtualenvwrapper, que ajuda bastante no dia a dia para quem precisa gerenciar várias aplicações; já no caso das versões do Python, podemos utilizar o pyenv, que também funciona com um arquivo de texto, para pinar as versões suportadas por arquele projeto. Enfim, são diversas as opções de utilizar "somente" o pip e ir complementando com diversas outras ferramentas no decorrer dos projetos - lembrando ainda que o pip realmente não foi criado para fazer isso tudo no qual necessitamos, por isso o nome dele é package installer for python.

Pipenv e Poetry: onde entram nessa história?

O npm é um exemplo de ferramenta que lida com esses problemas e é bastante conhecida. Além de instalar o que é preciso faz toda a validação, separa o que é para desenvolvimento e produção, entre outras coisas. É isso que o Pipenv e o Poetry também fazem para o ambiente de desenvolvimento de aplicações Python. Tudo o que foi citado na seção anterior eles fazem e um pouco mais, mas, por que não explicar somente sobre um e já sair utilizando? Em volta dessas ferramentas está muita polêmica na comunidade dev. Por que as duas coexistem se praticamente fazem a mesma coisa?

O Pipenv foi acolhido pela Python Packaging Authority, o grupo que mantém os projetos oficiais do Python para packaging, como a principal ferramenta para gerenciar dependências. De fato, assim que ela foi lançada, houve grande apoio de toda a comunidade por não termos ainda nenhuma ferramenta que fizesse o que o Pipenv fazia.

Logo no início, alguns desenvolvedores já demonstravam algumas preocupações com a rápida adoção a essa ferramenta, pois não havia sequer uma cobertura de testes automatizados descentes para a mesma. Com o passar do tempo, foram aparecendo alguns problemas que prejudicavam bastante como o Pipenv era visto, entre eles: pouca atualização (2 anos sem nenhuma nova release até que voltaram a lançar, em 2020), problemas de compatibilidade com o pip (quase toda atualização do pip o Pipenv quebrava fazendo com que todos projetos que dependiam do Pipenv quebrassem juntos), além de ser muito lento e consumir muita CPU. Esses foram os principais pontos para o projeto Poetry nascer, tentando trazer um ambiente mais seguro e eficaz para gerenciar dependências.

Uma breve tabela das features das duas ferramentas:

FeaturePipenvPoetry
Versão comparada2021.5.291.1.11
Locking de dependências
Gerenciamento de virtualenvs
requirements.txt export
Shell completion (bash, zsh, fish)
Gerenciamento de versões do Python
Publicação de pacote

Como podemos ver, as duas ferramentas são muito parecidas, o que nos leva a pensar sobre qual delas escolher, processo que às vezes é um pouco nebuloso. Nesse caso, analisamos a diferença de performance de cada uma para buildar as dependências e determinar qual é a melhor opção.

Configurações e ambiente utilizado para benchmark

Todos os testes foram feitos com a mesma configuração e sem executar nenhuma operação considerada pesada no computador enquanto executava os benchmarks.

OS: Ubuntu 20.04.3 LTS x86_64

Kernel: 5.11.0-38-generic

CPU: Intel(R) Core(TM) i7-10510U (8) CPU @ 1.80GHz

CPUs: 8

Memory: 16Gb

Shell: zsh 5.8

Python version: 3.9

Pipenv version: 2021.5.29

Poetry version: 1.1.11

Pacotes definidos para os testes

Para realização dos testes a seguir, utilizaremos um serviço que possui as seguintes dependências:

Packages:

PackageVersion
python"^3.9"
psycopg2-binary"2.8.4"
dj-database-url"0.5.0"
django"3.1"
django-cors-headers"3.5.0"
django-extensions"3.1.2"
django-filter"2.4.0"
djangorestframework"3.12.4"
dnspython"1.16.0"
elastic-apm"5.8.1"
graphene-django"2.13.0"
graphene-django-extras"0.4.9"
graphene-federation"0.1.0"
graphene-sentry"0.4.0"
gunicorn"19.9.0"
idna_ssl"1.1.0"
nameko"2.12.0"
nameko-amqp-retry"0.7.1"
nameko-sentry"1.0.0"
python-decouple"3.1"
sentry-sdk"0.14.1"
Unipath"1.1"
werkzeug"0.16.0"
xlsxwriter"1.3.2"

Dev packages:

PackageVersion
flake8"3.7.8"
coverage"4.5.4"
model-bakery"1.0.1"
ipython"7.8.0"
black"^19.10b0"
pytest"6.0.2"
pytest-django"3.10.0"

Pipenv

Instalação de dependências (com dependências de desenvolvimento)

Creating a virtualenv for this project...
Pipfile: /home/renan/Instruct/msets/services/internal/license_management/Pipfile
Using /home/renan/.asdf/installs/python/3.9.7/bin/python3.9 (3.9.7) to create virtualenv...
⠸ Creating virtual environment...created virtual environment CPython3.9.7.final.0-64 in 167ms
  creator CPython3Posix(dest=/home/renan/.local/share/virtualenvs/license_management-2VyCnzWO, clear=False, no_vcs_ignore=False, global=False)
  seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/home/renan/.local/share/virtualenv)
    added seed packages: pip==21.2.4, setuptools==58.2.0, wheel==0.37.0
  activators BashActivator,CShellActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator

✔ Successfully created virtual environment! 
Virtualenv location: /home/renan/.local/share/virtualenvs/license_management-2VyCnzWO
Installing dependencies from Pipfile.lock (de7292)...
  🐍   ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 84/84 — 00:00:32
To activate this project's virtualenv, run pipenv shell.
Alternatively, run a command inside the virtualenv with pipenv run.
pipenv install --dev  143,29s user 8,54s system 419% cpu 36,159 total

Neste primeiro teste, instalamos todas as dependências (inclusive as dev) com o comando pipenv install --dev.

Adição de nova dependência

Installing model-bakery...
Adding model-bakery to Pipfile's [dev-packages]...
✔ Installation Succeeded 
Pipfile.lock (de7292) out of date, updating to (7ada07)...
Locking [dev-packages] dependencies...
Building requirements...
Resolving dependencies...
✔ Success! 
Locking [packages] dependencies...
Building requirements...
Resolving dependencies...
✔ Success! 
Updated Pipfile.lock (7ada07)!
Installing dependencies from Pipfile.lock (7ada07)...
  🐍   ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 0/0 — 00:00:00
To activate this project's virtualenv, run pipenv shell.
Alternatively, run a command inside the virtualenv with pipenv run.
pipenv install --dev model-bakery  18,07s user 0,63s system 71% cpu 26,270 total

Observamos como foi próximo o tempo de instalação de todas as dependências (36,159 segundos), com o tempo de adição de uma nova (26,270 segundos), a maior parte deste tempo, foi gasto na etapa de locking.

Lock de dependências

Locking [dev-packages] dependencies...
Building requirements...
Resolving dependencies...
✔ Success! 
Locking [packages] dependencies...
Building requirements...
Resolving dependencies...
✔ Success! 
Updated Pipfile.lock (7ada07)!
pipenv lock  13,88s user 0,44s system 90% cpu 15,903 total

Remoção de uma dependência

Uninstalling model-bakery...
Found existing installation: model-bakery 1.3.3
Uninstalling model-bakery-1.3.3:
  Successfully uninstalled model-bakery-1.3.3

Removing model-bakery from Pipfile...
Locking [dev-packages] dependencies...
Building requirements...
Resolving dependencies...
✔ Success! 
Locking [packages] dependencies...
Building requirements...
Resolving dependencies...
✔ Success! 
Updated Pipfile.lock (de7292)!
pipenv uninstall model-bakery  13,99s user 0,48s system 90% cpu 15,916 total

Remoção de uma dependência foi um pouco mais rápida, mas novamente a maior parte do tempo foi gasta na etapa de locking.

Poetry

Instalação de dependências (com dependências de desenvolvimento)

Creating virtualenv license-management-DNNlYm44-py3.9 in /home/renan/.cache/pypoetry/virtualenvs
Installing dependencies from lock file

Package operations: 87 installs, 0 updates, 0 removals

  • Installing six (1.16.0)
  • Installing promise (2.3)
  • Installing rx (1.6.1)
  • Installing graphql-core (2.3.2)
  • Installing vine (1.3.0)
  • Installing amqp (2.6.1)
  • Installing aniso8601 (7.0.0)
  • Installing asgiref (3.2.10)
  • Installing certifi (2021.10.8)
  • Installing charset-normalizer (2.0.7)
  • Installing dnspython (1.16.0)
  • Installing graphql-relay (2.0.1)
  • Installing greenlet (1.1.2)
  • Installing idna (3.3)
  • Installing path (16.2.0)
  • Installing pyparsing (2.4.7)
  • Installing pytz (2021.3)
  • Installing sqlparse (0.4.2)
  • Installing urllib3 (1.26.7)
  • Installing attrs (21.2.0)
  • Installing django (3.1)
  • Installing eventlet (0.32.0)
  • Installing graphene (2.1.9)
  • Installing iniconfig (1.1.1)
  • Installing kombu (4.6.11)
  • Installing mock (4.0.3)
  • Installing more-itertools (8.10.0)
  • Installing packaging (21.2)
  • Installing parso (0.8.2)
  • Installing path.py (12.5.0)
  • Installing pluggy (0.13.1)
  • Installing ptyprocess (0.7.0)
  • Installing py (1.11.0)
  • Installing pyyaml (6.0)
  • Installing requests (2.26.0)
  • Installing singledispatch (3.7.0)
  • Installing toml (0.10.2)
  • Installing unidecode (1.3.2)
  • Installing wcwidth (0.2.5)
  • Installing werkzeug (0.16.0)
  • Installing wrapt (1.13.3)
  • Installing appdirs (1.4.4)
  • Installing backcall (0.2.0)
  • Installing click (8.0.3)
  • Installing decorator (5.1.0)
  • Installing django-filter (2.4.0)
  • Installing djangorestframework (3.12.4)
  • Installing entrypoints (0.3)
  • Installing graphene-django (2.13.0)
  • Installing graphene-file-upload (1.2.2)
  • Installing jedi (0.18.0)
  • Installing mccabe (0.6.1)
  • Installing nameko (2.12.0)
  • Installing pathspec (0.9.0)
  • Installing pexpect (4.8.0)
  • Installing pickleshare (0.7.5)
  • Installing prompt-toolkit (2.0.10)
  • Installing pycodestyle (2.5.0)
  • Installing pyflakes (2.1.1)
  • Installing pygments (2.10.0)
  • Installing pytest (6.0.2)
  • Installing python-dateutil (2.8.2)
  • Installing raven (6.10.0)
  • Installing regex (2021.11.2)
  • Installing sentry-sdk (0.14.1)
  • Installing traitlets (5.1.1)
  • Installing typed-ast (1.4.3)
  • Installing black (19.10b0)
  • Installing coverage (4.5.4)
  • Installing dj-database-url (0.5.0)
  • Installing django-cors-headers (3.5.0)
  • Installing django-extensions (3.1.2)
  • Installing elastic-apm (5.8.1)
  • Installing flake8 (3.7.8)
  • Installing graphene-django-extras (0.4.9)
  • Installing graphene-federation (0.1.0)
  • Installing graphene-sentry (0.4.0)
  • Installing gunicorn (19.9.0)
  • Installing idna-ssl (1.1.0)
  • Installing ipython (7.8.0)
  • Installing nameko-amqp-retry (0.7.1)
  • Installing nameko-sentry (1.0.0)
  • Installing psycopg2-binary (2.8.4)
  • Installing pytest-django (3.10.0)
  • Installing python-decouple (3.1)
  • Installing unipath (1.1)
  • Installing xlsxwriter (1.3.2)

Installing the current project: license_management (0.1.0)
poetry install  79,92s user 8,26s system 411% cpu 21,454 total

Instalação de todas as dependências com o comando poetry install. O poetry, ao contrário do Pipenv, já instala as dependências de desenvolvimento por default.

Adição de nova dependência

Using version ^1.3.3 for model-bakery

Updating dependencies
Resolving dependencies... (0.0s)PackageInfo: Invalid constraint (0.6 ; extra == 'rq') found in sentry-sdk-0.14.1 dependencies, skipping
Resolving dependencies... (0.5s)

Writing lock file

Package operations: 1 install, 0 updates, 0 removals

  • Installing model-bakery (1.3.3)
poetry add --dev model-bakery  2,69s user 0,22s system 90% cpu 3,226 total

Grande destaque aqui no tempo gasto pelo Poetry ao adicionar uma nova dependência (3,226 segundos), devido ao processo de locking otimizado do mesmo que, ao invés de fazer lock de todas as dependências já instaladas, faz somente do novo package.

Lock de dependências

Updating dependencies
Resolving dependencies... (3.1s)
Resolving dependencies... (5.1s)
poetry lock  3,76s user 0,16s system 61% cpu 6,331 total

Aqui também temos um processo de locking acontecendo bem mais rápido do que no Pipenv, e consumindo menos CPU (61% no poetry vs 90% no pipenv).

Remoção de uma dependência

Updating dependencies
Resolving dependencies... (0.0s)
Resolving dependencies... (0.3s)

Writing lock file

Package operations: 0 installs, 0 updates, 1 removal

  • Removing model-bakery (1.3.3)
poetry remove --dev model-bakery  2,59s user 0,14s system 99% cpu 2,746 total

Novamente, uma remoção de dependência acontecendo em menos de 3 segundos no Poetry, enquanto o Pipenv levou 15,916 segundos.

Resultados dos testes

TestePipenv - Tempo decorrido (em segundos)Pipenv - Uso CPUPoetry - Tempo decorrido (em segundos)Poetry - Uso CPU
Instalação das dependências36,159419%*21,454411%*
Adição de nova dependência26,27071%*3,22690%*
Lock de dependências15,90390%*6,33161%*
Remoção de dependência15,91690%*2,74699%*

* A porcentagem indicada é por CPU, e não em relação ao total de CPUs disponíveis

Documentação

Outra grande diferença entre esses dois gerenciadores, está na documentação de cada um deles.

O Poetry possui uma ótima documentação, clara, objetiva e fácil de utilizar.

Já no caso do Pipenv, no momento em que este artigo está sendo escrito (novembro de 2021), a documentação oficial indicada no github do projeto se encontra fora do ar, apresentando erro SSL_ERROR_BAD_CERT_DOMAIN.

Resolução de dependências

Durante a execução dos testes realizados neste artigo, um grave problema foi descoberto na maneira que o Pipenv faz a sua resolução de dependências. Observem o seguinte cenário:

Em um ambiente limpo, vamos instalar o Django, explicitando sua versão em 2.1

Creating a virtualenv for this project...
Pipfile: /home/renan/tests/test_dependency_error/Pipfile
Using /home/renan/.asdf/installs/python/3.9.7/bin/python3 (3.9.7) to create virtualenv...
⠼ Creating virtual environment...created virtual environment CPython3.9.7.final.0-64 in 214ms
  creator CPython3Posix(dest=/home/renan/.local/share/virtualenvs/test_dependency_error-3YHoegjO, clear=False, no_vcs_ignore=False, global=False)
  seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/home/renan/.local/share/virtualenv)
    added seed packages: pip==21.2.4, setuptools==58.2.0, wheel==0.37.0
  activators BashActivator,CShellActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator

✔ Successfully created virtual environment! 
Virtualenv location: /home/renan/.local/share/virtualenvs/test_dependency_error-3YHoegjO
Creating a Pipfile for this project...
Installing django==2.1...
Adding django to Pipfile's [packages]...
✔ Installation Succeeded 
Pipfile.lock not found, creating...
Locking [dev-packages] dependencies...
Locking [packages] dependencies...
Building requirements...
Resolving dependencies...
✔ Success! 
Updated Pipfile.lock (f872d3)!
Installing dependencies from Pipfile.lock (f872d3)...
  🐍   ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 0/0 — 00:00:00
To activate this project's virtualenv, run pipenv shell.
Alternatively, run a command inside the virtualenv with pipenv run.

Agora, iremos instalar como dependência dev, o Django Rest Framework. Importante reparar aqui, que o Django Rest Framework exige o Django 2.2 como versão mínima:

Requirements

REST framework requires the following:

    Python (3.5, 3.6, 3.7, 3.8, 3.9)
    Django (2.2, 3.0, 3.1, 3.2)

Resultado da instalação:

Installing djangorestframework...
Adding djangorestframework to Pipfile's [dev-packages]...
✔ Installation Succeeded 
Pipfile.lock (f872d3) out of date, updating to (388f4b)...
Locking [dev-packages] dependencies...
Building requirements...
Resolving dependencies...
✔ Success! 
Locking [packages] dependencies...
Building requirements...
Resolving dependencies...
✔ Success! 
Updated Pipfile.lock (388f4b)!
Installing dependencies from Pipfile.lock (388f4b)...
  🐍   ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 1/1 — 00:00:00
To activate this project's virtualenv, run pipenv shell.
Alternatively, run a command inside the virtualenv with pipenv run.

Para deixar mais explícito o problema, exibiremos as dependências do projeto com o comando pipenv graph:

asgiref==3.4.1
djangorestframework==3.12.4
  - django [required: >=2.2, installed: 2.1]
    - pytz [required: Any, installed: 2021.3]
sqlparse==0.4.2

O Pipenv permitiu a instalação do Django Rest Framework, mesmo tendo o Django em uma versão inferior a mínima especificada.

Este problema acontece pelo fato do Pipenv gerar dois locks diferentes de maneira independente para prod e dev, e as dependências de produção sobrescrevem as dependências de desenvolvimento.

Em uma breve pesquisa, foram encontradas várias issues (#3905, #4037, #4724) relatando este problema, e aparentemente este é um bug conhecido que acontece desde 2019, conforme conta um dos próprios membros na pypa nessa issue reportada.

Ao tentar reproduzir o mesmo cenário com o Poetry, ele imediatamente faz a detecção da versão inferior do Django e não permite a instalação do Django Rest Framweork:

Using version ^3.12.4 for djangorestframework

Updating dependencies
Resolving dependencies... (0.0s)

  SolverProblemError

  Because no versions of djangorestframework match >3.12.4,<4.0.0
   and djangorestframework (3.12.4) depends on django (>=2.2), djangorestframework (>=3.12.4,<4.0.0) requires django (>=2.2).
  So, because test-dependency-error depends on both django (2.1) and djangorestframework (^3.12.4), version solving failed.

  at ~/.poetry/lib/poetry/puzzle/solver.py:241 in _solve
      237│             packages = result.packages
      238│         except OverrideNeeded as e:
      239│             return self.solve_in_compatibility_mode(e.overrides, use_latest=use_latest)
      240│         except SolveFailure as e:
    → 241│             raise SolverProblemError(e)
      242│ 
      243│         results = dict(
      244│             depth_first_search(
      245│                 PackageNode(self._package, packages), aggregate_package_nodes

Resultados

Pipenv é mantido por um órgão oficial da comunidade Python e por ser mais conhecido tem suporte há um pouco mais de tempo, como é o caso do buildpack python para o Heroku suportar o Pipenv mas não o Poetry.

Poetry consegue ter um CLI bem intuitivo, por exemplo separar o install do add, e também permite a criação de pacotes que podem ser publicados no Pypi ou qualquer outro repositório python. Outro grande diferencial do Poetry é ele seguir a PEP 518.

Como ficou claro nos testes, apesar de ambas as ferramentas manterem um consumo bem próximo de CPU, o Pipenv se mostrou muito mais lento em relação ao Poetry, pois para todas operações ele baixa, instala e recria o lockfile. Também em experiências passadas tivemos problemas de o Pipenv deixar dependências órfãs, não desinstalando as dependências das dependências, mas isso não ocorreu nesse teste feito com sua última release. Nenhuma dessas ferramentas será tão rápida quanto o pip, pois, por motivos de segurança e consistência das dependências, elas fazem um processo sequencial de instalação dos nós filhos (dependências mais profundas do grafo) até o nó pai.

Temos também, conforme foi demonstrado, um antigo, grave, e já conhecido problema na resolução de dependências do Pipenv, onde não é respeitada as versões mínimas entre packages de produção e de desenvolvimento.

Espero que esse post ajude na escolha futura de quem precisar escolher como gerenciar dependências para seus projetos.

Solução de problemas, melhoria de gestão e aumento de produtividade

Com tecnologias renomadas, aplicamos práticas ágeis e capacitamos sua equipe.

Fale com um consultor