Ambientes Virtuais em Python
Modularidade e empacotamento são recursos presentes em toda linguagem de programação relevante, pois permitem que nós, desenvolvedores, reutilizemos facilmente trechos de código recorrentes (também conhecidos como módulos).
Em Python, podemos adicionar pacotes como dependências do nosso projeto instalando-os com algum gerenciador de pacotes, como o pip:
pip install requests==2.30.0
pip install pytest pytest-cov
No exemplo acima, estamos instalando a biblioteca requests na versão 2.30.0 na primeira linha e, na segunda, as bibliotecas pytest e pytest-cov. Como não estamos especificando uma versão para pytest e pytest-cov, as versões mais recentes serão instaladas.
Isso é ótimo, mas surge um problema quando trabalhamos em vários projetos simultaneamente, compartilhamos projetos com outras pessoas ou até mesmo usamos a máquina para estudar. O gerenciamento de pacotes específicos por projeto torna-se desafiador.
Desvantagens de instalações globais (sem ambientes virtuais)
Antes de falarmos sobre ambientes virtuais, vamos imaginar uma situação. Suponha que você começou um novo projeto e precise instalar pandas, requests e jinja para ele. Você simplesmente executa:
pip install Jinja2 requests pandas
Depois disso, você compartilha o projeto com outra pessoa, usando git para versioná-lo e enviá-lo para um repositório remoto como o GitHub. Agora, outras pessoas precisam contribuir com o projeto, mas ao tentar executá-lo, veem uma mensagem de erro dizendo “no module named ‘pandas’”. Isso faz sentido, já que você está compartilhando apenas o código-fonte no repositório e não todo o seu ambiente (seu interpretador python, pacotes instalados, etc.).

Para resolver o problema, você poderia simplesmente instruir as pessoas do projeto a executarem pip install Jinja2 requests pandas.
Mas isso não resolve completamente. Algumas pessoas podem já ter versões diferentes do requests instaladas em suas máquinas, o que pode não gerar o mesmo erro, mas sim erros diferentes devido a incompatibilidades entre versões.
Congelando dependências
Você precisa de uma forma de compartilhar exatamente as mesmas versões das dependências que está usando no projeto. Felizmente, o Python traz uma solução para isso: o subcomando freeze do pip.
Com ele, você pode listar todas as dependências instaladas (inclusive as dependências das suas dependências) com as versões correspondentes e salvar em um arquivo de texto, que depois pode ser usado para instalar tudo corretamente:
pip freeze > requirements.txt
O comando acima é um padrão comum no Python para criar um arquivo requirements.txt com as dependências atualmente instaladas (ele “congela” suas versões). Depois, ao configurar o projeto em uma nova máquina, basta executar:
pip install -r requirements.txt
Esse comando instalará tudo o que está listado no seu requirements.txt.
Nota: mantenha este arquivo na raiz do seu projeto para facilitar o fluxo de trabalho.
Agora temos um comando simples para instalar todas as dependências, e só precisamos garantir que o requirements.txt esteja atualizado executando pip freeze > requirements.txt sempre que incluirmos uma nova dependência no projeto, certo?
Não exatamente. Ainda há um problema.
Pacotes instalados globalmente
Lembra quando instalamos requests 2.30, pytest e pytest-cov no início deste artigo? Essas bibliotecas não fazem parte do projeto que criamos depois, mas mesmo assim você notará que seu requirements.txt as inclui (junto com toda a árvore de dependências). Veja só:

Isso pode ser um problema, pois instalará pacotes desnecessários e pode até afetar a resolução de dependências do pip install.
Além disso, imagine a bagunça ao precisar trabalhar com versões diferentes do mesmo pacote em projetos distintos na mesma máquina…
Para resolver isso, podemos usar os ambientes virtuais do Python.
venv do Python
O ambiente virtual em Python — ou venv — é um recurso para isolar o contexto do seu projeto (interpretador, bibliotecas instaladas etc.) da configuração global do Python.
Na prática, os pacotes instalados dentro de um ambiente virtual não entram em conflito com os pacotes globais da máquina. É uma boa prática criar um venv para cada projeto.
A seguir, veremos um passo a passo de como usar e entender o venv.
Criar um venv
O Python vem com um módulo interno chamado venv para criar ambientes virtuais.
Você pode criar um novo ambiente assim:
python -m venv venv
# nota: no seu sistema, o comando pode ser python3 ou py em vez de python...
No comando acima, python -m venv cria o ambiente virtual e o último venv é apenas o nome da pasta que será criada (pode ser outro nome, mas “venv” é o mais comum).
Essa pasta conterá o interpretador Python, bibliotecas instaladas etc.
Nota: Não se preocupe com a pasta
venvgerada — o Python gerencia tudo internamente, você não precisa mexer nela.
Seu venv está criado, mas ainda não está ativo, então você continua no ambiente normal.
Ativar o venv
Agora que temos um venv para o projeto, podemos ativá-lo:
# Linux e MacOS:
source venv/bin/activate
# Windows:
.\venv\Scripts\activate
Você notará que “venv” (ou o nome que escolheu) aparece em verde no terminal, indicando que o ambiente foi ativado com sucesso.
Seu shell permanece praticamente o mesmo — apenas o interpretador Python é trocado pelo do venv.
Trabalhando dentro do venv
A primeira coisa a notar é que você não tem acesso aos pacotes instalados globalmente.
Por exemplo, tentar importar pandas gerará ModuleNotFoundError, o que faz sentido, pois o ambiente é novo e está isolado da sua instalação global.
Vamos reinstalar as dependências necessárias e congelá-las:
pip install Jinja2 requests pandas
pip freeze > requirements.txt
Você pode usar os comandos python e pip normalmente.
A diferença é que aqui eles apontam para o ambiente virtual, não para a instalação global.
Veja a diferença entre o requirements.txt anterior (à esquerda) e o novo, gerado dentro do venv (à direita):

De 22 dependências, passamos para 13 — uma redução de 9 dependências, ou cerca de 40% a menos!
Nota: Quando você fecha o terminal ou o IDE, o venv é desativado. Então, sempre que abrir novamente, será necessário reativá-lo.
Desativar o venv
Quando quiser voltar ao ambiente global, basta rodar:
deactivate
Considerações finais
Perfeito — agora podemos trabalhar com ambientes virtuais e ter um contexto isolado por projeto. Mas ainda há algumas observações importantes.
Git
A pasta do ambiente virtual contém muitos arquivos e costuma ser grande.
Além disso, o gerenciador de pacotes pode se comportar de forma diferente em cada máquina, então é importante adicionar a pasta venv ao .gitignore.
Assim, você mantém apenas o código-fonte no repositório, e qualquer pessoa pode reproduzir o ambiente instalando as dependências.
Pipelines de CI/CD
Em pipelines de CI/CD normalmente já existe um ambiente isolado, então não há necessidade de criar e ativar um venv antes de instalar as dependências.
Você pode ir direto para o pip install -r requirements.txt e executar o código.
Usuários do VSCode
Existem várias extensões legais de Python no VSCode, mas a essencial, na minha opinião, é a Python da Microsoft.

Essa extensão traz recursos como intellisense, debugging, entre outros. Para aproveitar tudo, certifique-se de que o interpretador Python selecionado no VSCode é o do seu venv.
Suponha que adicionamos uma nova dependência ao projeto (apenas no venv):
pip install matplotlib
pip freeze > requirements.txt
Veja o que acontece se não estivermos usando o interpretador correto:

Note que temos o matplotlib instalado no venv, mas o VSCode ainda mostra um aviso de que o pacote não foi encontrado.
Se tentarmos executar o script pelo botão “run”:

Agora, executando o mesmo script pelo terminal com o venv ativo:

Isso mostra que o VSCode está usando o interpretador errado. Para corrigir, basta clicar no canto inferior direito (onde aparece o Python ativo, em vermelho na imagem) e selecionar o interpretador correto no menu que será exibido (você o identifica pelo caminho do venv, também destacado em vermelho).

Após isso, os avisos somem, o intellisense volta a funcionar e o botão de execução roda sem erros:

No canto inferior direito, você verá o interpretador do seu venv sendo exibido.
Em conclusão, os ambientes virtuais são uma ferramenta poderosa para gerenciar dependências e criar contextos isolados em Python. Eles garantem instalações consistentes e evitam conflitos entre projetos diferentes. Se quiser se aprofundar no tema, confira também: https://realpython.com/python-virtual-environments-a-primer/