À medida que um programa cresce, colocar tudo em um único arquivo se torna insustentável. Módulos e pacotes são o mecanismo do Python para dividir código em unidades organizadas, reutilizáveis e independentes. Entender como o Python resolve importações e como estruturar um projeto profissional é essencial para qualquer desenvolvedor que queira além de scripts simples.
O que é um Módulo?
Um módulo é simplesmente um arquivo .py. Qualquer arquivo Python é automaticamente um módulo e pode ser importado por outros arquivos.
# arquivo: matematica.py
PI = 3.14159265358979
def area_circulo(raio):
"""Calcula a área de um círculo."""
return PI * raio ** 2
def area_retangulo(largura, altura):
"""Calcula a área de um retângulo."""
return largura * altura
def mdc(a, b):
"""Máximo divisor comum — algoritmo de Euclides."""
while b:
a, b = b, a % b
return a
# arquivo: main.py
import matematica
print(matematica.PI)
print(matematica.area_circulo(5))
print(matematica.mdc(48, 18))
Formas de Importação
# Importa o módulo inteiro — acesso via nome do módulo
import matematica
print(matematica.PI)
# Importa com alias — útil para nomes longos
import matematica as mat
print(mat.area_circulo(3))
# Importa nomes específicos — acesso direto
from matematica import PI, area_circulo
print(PI)
print(area_circulo(3))
# Importa tudo — evite, polui o namespace
from matematica import *
# Módulos da biblioteca padrão
import os
import sys
import json
from datetime import datetime, timedelta
from pathlib import Path
from collections import defaultdict, Counter
O Atributo name
Cada módulo tem um atributo __name__. Quando executado diretamente, vale "__main__"; quando importado, vale o nome do arquivo:
# arquivo: utilitarios.py
def somar(a, b):
return a + b
def subtrair(a, b):
return a - b
if __name__ == "__main__":
# Este bloco só executa quando o arquivo é rodado diretamente
# Não executa quando o módulo é importado
print("Testando utilitarios.py")
print(somar(3, 4)) # 7
print(subtrair(10, 3)) # 7
Esse padrão é fundamental — permite que um arquivo funcione tanto como módulo reutilizável quanto como script independente.
Pacotes
Um pacote é um diretório que contém módulos e, por convenção, um arquivo __init__.py. Esse arquivo pode estar vazio ou conter código de inicialização do pacote.
Uma correção que vale fazer logo: o __init__.py deixou de ser obrigatório no Python 3.3, com a introdução dos pacotes de espaço de nomes. Um diretório sem ele é importável do mesmo jeito, e quem aprendeu que o arquivo é o que define um pacote acaba perdendo tempo procurando o motivo de algo funcionar sem ele.
Ainda assim, criá-lo continua sendo a recomendação, por três razões práticas. Ele dá um lugar para expor a interface pública do pacote, reunindo num ponto só os nomes que valem a pena importar de fora. Ele torna o pacote regular, o que evita um comportamento surpreendente dos pacotes de espaço de nomes: como eles podem se espalhar por vários diretórios do sys.path, um erro de digitação no nome de uma pasta produz um pacote vazio e importável em vez de um ModuleNotFoundError. E ferramentas de empacotamento e de descoberta de testes ainda tratam os dois casos de forma diferente. A regra prática: use pacote de espaço de nomes quando quiser mesmo dividir um pacote entre distribuições separadas, e __init__.py em todo o resto.
meu_projeto/
├── __init__.py
├── matematica/
│ ├── __init__.py
│ ├── basica.py
│ ├── geometria.py
│ └── estatistica.py
├── utils/
│ ├── __init__.py
│ ├── arquivos.py
│ └── strings.py
└── main.py
# matematica/geometria.py
def area_circulo(raio):
import math
return math.pi * raio ** 2
def volume_esfera(raio):
import math
return (4/3) * math.pi * raio ** 3
# matematica/__init__.py
from .basica import somar, subtrair, mdc
from .geometria import area_circulo, volume_esfera
__all__ = ["somar", "subtrair", "mdc", "area_circulo", "volume_esfera"]
# main.py
from matematica import area_circulo, somar
from matematica.geometria import volume_esfera
print(area_circulo(5))
print(volume_esfera(3))
print(somar(2, 3))
Importações Relativas
Dentro de um pacote, use importações relativas para referenciar módulos do mesmo pacote:
# matematica/estatistica.py
from .basica import somar # importa do mesmo pacote
from ..utils.strings import formatar # importa do pacote pai
def media(valores):
return somar(*valores) / len(valores)
. refere-se ao pacote atual; .. refere-se ao pacote pai.
O que decide até onde o .. pode subir não é a estrutura de pastas em disco: é como o código foi importado. O mesmo arquivo funciona ou falha conforme o ponto de entrada.
# de fora de meu_projeto/, com o pacote inteiro visível:
python -c "from meu_projeto.matematica.estatistica import media"
# funciona
# de dentro de meu_projeto/, onde matematica passa a ser o topo:
python -c "from matematica.estatistica import media"
# ImportError: attempted relative import beyond top-level package
No segundo caso o .. tentaria sair do pacote mais alto que o Python conhece, e não existe nada acima disso — o interpretador não sobe para o sistema de arquivos à procura. É a causa mais comum de importação relativa que funcionava ontem
: ninguém mexeu no código, mudou-se o diretório de onde o programa é chamado. Daí duas práticas que evitam o problema. Execute módulos de dentro de pacotes com python -m meu_projeto.matematica.estatistica, e não com python caminho/para/estatistica.py, porque a segunda forma põe o arquivo como topo e desmonta a hierarquia. E prefira importação absoluta — from meu_projeto.utils.strings import formatar — em qualquer travessia que suba de pacote, como a PEP 8 recomenda: é mais longa, e é a que não depende de onde o programa foi iniciado.
Ambiente Virtual
Antes de instalar dependências externas, crie sempre um ambiente virtual — um espaço isolado que mantém as dependências de cada projeto separadas:
# Criar ambiente virtual — Linux/macOS
python3 -m venv venv
# Criar ambiente virtual — Windows
py -m venv venv
# Ativar — Linux/macOS
source venv/bin/activate
# Ativar — Windows
venv\Scripts\activate
# Desativar
deactivate
O nome do comando é a primeira pedra do caminho, e varia. No Windows, python3 normalmente não existe — há o py, que é o lançador oficial e aceita escolher a versão com py -3.12. Em muitas distribuições Linux e no macOS existe só python3, porque python foi reservado para a versão 2 por anos. E há ambientes em que ocorre o inverso, com apenas python apontando para uma versão 3 recente. Antes de copiar qualquer comando, confirme com python --version e python3 --version qual dos dois responde, e o quê.
Uma vez ativado o ambiente, o problema desaparece: dentro dele, python e pip apontam para o ambiente virtual em qualquer sistema. Vale ainda saber que python -m pip install ... é mais seguro que pip install ..., porque garante que o pip usado é o do interpretador que se pretende — num computador com várias versões instaladas, o pip solto no PATH pode ser de outra.
Com o ambiente ativo, instale pacotes com pip:
pip install requests
pip install pandas numpy matplotlib
pip install fastapi uvicorn
Gerenciamento de Dependências
# Gerar arquivo de dependências
pip freeze > requirements.txt
# Instalar dependências de um arquivo
pip install -r requirements.txt
# Ver pacotes instalados
pip list
# Informações sobre um pacote
pip show requests
Vale uma ressalva sobre o pip freeze, porque ele é menos útil do que parece: ele registra tudo o que está instalado, sem distinguir o que o projeto pediu do que veio a reboque. Pedir pip install fastapi instala mais de uma dezena de pacotes, e o freeze lista os doze como se fossem decisões do projeto. O arquivo resultante é difícil de manter — ninguém sabe quais linhas podem sair —, e costuma ficar poluído com o que alguém instalou para um teste e esqueceu.
A prática que envelhece melhor é manter à mão um arquivo com as dependências diretas, apenas o que o código realmente importa, e deixar o resolvedor cuidar do resto. Para isso o caminho moderno é declarar as dependências no pyproject.toml, adiante nesta mesma aula, e gerar o arquivo travado a partir dele com ferramentas como pip-tools, uv ou Poetry — assim existem os dois arquivos, cada um com seu papel: um legível, que as pessoas editam, e um completo, que garante que a instalação seja idêntica em toda máquina.
Conteúdo típico de requirements.txt:
requests==2.31.0
pandas==2.1.0
numpy==1.26.0
fastapi==0.104.0
uvicorn==0.24.0
Estrutura de Projeto Profissional
Uma estrutura bem organizada facilita testes, manutenção e colaboração:
meu_projeto/
├── src/
│ └── meu_projeto/
│ ├── __init__.py
│ ├── modelos/
│ │ ├── __init__.py
│ │ ├── usuario.py
│ │ └── produto.py
│ ├── servicos/
│ │ ├── __init__.py
│ │ ├── autenticacao.py
│ │ └── email.py
│ ├── repositorios/
│ │ ├── __init__.py
│ │ └── usuario_repo.py
│ └── utils/
│ ├── __init__.py
│ └── validacao.py
├── tests/
│ ├── __init__.py
│ ├── test_modelos.py
│ └── test_servicos.py
├── docs/
├── .env
├── .gitignore
├── pyproject.toml
├── requirements.txt
└── README.md
pyproject.toml
O arquivo moderno de configuração de projetos Python, substituindo o antigo setup.py:
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.backends.legacy:build"
[project]
name = "meu-projeto"
version = "0.1.0"
description = "Descrição do projeto"
requires-python = ">=3.11"
dependencies = [
"requests>=2.31.0",
"fastapi>=0.104.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.4.0",
"black>=23.0.0",
"ruff>=0.1.0",
]
[tool.black]
line-length = 88
[tool.ruff]
line-length = 88
select = ["E", "F", "I"]
O Sistema de Importação por Dentro
Quando você escreve import matematica, Python executa os seguintes passos:
- Verifica
sys.modules— se já importado, usa o cache - Busca o módulo na ordem: módulos embutidos →
sys.path - Carrega e executa o arquivo
.py - Armazena em
sys.modules
import sys
# Ver onde Python busca módulos
for caminho in sys.path:
print(caminho)
# Ver módulos já importados
print("json" in sys.modules) # True se json foi importado
# Adicionar caminho dinamicamente (evite em produção)
sys.path.insert(0, "/caminho/para/meus/modulos")
Exemplo Completo: Pacote de Utilitários
# utils/validacao.py
import re
from dataclasses import dataclass
from typing import List
@dataclass
class ResultadoValidacao:
valido: bool
erros: List[str]
def __bool__(self):
return self.valido
def validar_email(email: str) -> ResultadoValidacao:
erros = []
if not email:
erros.append("E-mail não pode ser vazio.")
elif not re.match(r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$", email):
erros.append("Formato de e-mail inválido.")
return ResultadoValidacao(valido=not erros, erros=erros)
def validar_senha(senha: str) -> ResultadoValidacao:
erros = []
if len(senha) < 8:
erros.append("Senha deve ter ao menos 8 caracteres.")
if not re.search(r"[A-Z]", senha):
erros.append("Senha deve conter ao menos uma letra maiúscula.")
if not re.search(r"\d", senha):
erros.append("Senha deve conter ao menos um número.")
if not re.search(r"[!@#$%^&*]", senha):
erros.append("Senha deve conter ao menos um caractere especial.")
return ResultadoValidacao(valido=not erros, erros=erros)
def validar_cadastro(dados: dict) -> ResultadoValidacao:
todos_erros = []
email_result = validar_email(dados.get("email", ""))
if not email_result:
todos_erros.extend(email_result.erros)
senha_result = validar_senha(dados.get("senha", ""))
if not senha_result:
todos_erros.extend(senha_result.erros)
return ResultadoValidacao(valido=not todos_erros, erros=todos_erros)
# utils/__init__.py expõe a interface pública do pacote
# from .validacao import validar_email, validar_senha, validar_cadastro
# main.py
from utils.validacao import validar_cadastro
cadastros = [
{"email": "ana@email.com", "senha": "Segura@123"},
{"email": "invalido", "senha": "fraca"},
{"email": "bruno@email.com","senha": "SemEspecial1"},
]
for dados in cadastros:
resultado = validar_cadastro(dados)
if resultado:
print(f"[OK] {dados['email']} — cadastro válido")
else:
print(f"[ERRO] {dados['email']}:")
for erro in resultado.erros:
print(f" - {erro}")
Módulo é qualquer arquivo .py e pacote é um diretório de módulos — a mecânica é simples, e o que costuma dar trabalho é o sistema de importação em volta dela. Vale reter três correções sobre o que muito material ainda ensina. O __init__.py deixou de ser obrigatório no Python 3.3, embora continue sendo recomendado, porque dá um lugar para a interface pública do pacote e evita que um erro de digitação em nome de pasta produza um pacote vazio e importável. O comando do interpretador não se chama igual em todo lugar: no Windows costuma ser py, em muitos Linux só existe python3, e conferir antes de copiar comando economiza a primeira meia hora. E o pip freeze registra tudo o que está instalado, sem distinguir o que o projeto pediu do que veio a reboque — o arquivo legível de dependências diretas é outro, e hoje mora no pyproject.toml.
A armadilha que mais custa tempo, porém, é a importação relativa. O alcance do .. não é determinado pela estrutura de pastas, e sim por como o programa foi iniciado: o mesmo arquivo importa sem problema quando o pacote de cima está visível e falha com attempted relative import beyond top-level package quando alguém o executa de dentro do diretório. É por isso que módulos dentro de pacotes se executam com python -m pacote.modulo, e não apontando para o arquivo, e é por isso que a PEP 8 prefere importação absoluta em qualquer travessia que suba de nível — mais longa de escrever, e independente de onde o programa foi chamado.
Fontes e leituras recomendadas
- Sistema de importação — documentação oficial — https://docs.python.org/3/reference/import.html
- Ambientes virtuais e pip — https://docs.python.org/3/tutorial/venv.html
- pyproject.toml — PEP 517 e 518 — https://peps.python.org/pep-0517/
- Guia de empacotamento Python — https://packaging.python.org/en/latest/
- BEAZLEY, David; JONES, Brian K. Python Cookbook. 3. ed. O'Reilly Media, 2013. Cap. 10 — módulos e pacotes em profundidade.
- HUNT, John. Advanced Guide to Python 3 Programming. Springer, 2019. Cap. 2 — estrutura de projetos e boas práticas.
- PERCIVAL, Harry; GREGORY, Bob. Architecture Patterns with Python. O'Reilly Media, 2020. Cap. 1 — organização de projetos orientados a domínio.
Exercícios
Exercício 1
Um módulo dentro de um pacote usa from ..utils.strings import formatar e funciona há meses. Um colega abre o terminal dentro da pasta do pacote e roda python estatistica.py para testar rapidamente uma função — e recebe ImportError: attempted relative import beyond top-level package. Nada no código mudou. Explique.
Ver resposta
✓ Resposta: O alcance de uma importação relativa não é decidido pela estrutura de pastas em disco, e sim pelo pacote ao qual o módulo pertence no momento da execução, que o Python guarda no atributo __package__. Quando o programa entra pelo pacote de cima, o módulo sabe que é meu_projeto.matematica.estatistica, e o .. sobe um nível até meu_projeto, onde utils existe. Ao executar o arquivo diretamente com python estatistica.py, a situação muda por completo: o Python trata aquele arquivo como um script de nível superior, com __name__ igual a "__main__" e sem pacote nenhum. O .. passa a tentar subir acima do topo, e não existe nada acima — o interpretador não sai procurando pastas no sistema de arquivos para descobrir que aquele diretório é parte de um projeto maior. Daí a mensagem. O mesmo acontece, de forma mais sutil, quando alguém entra na pasta meu_projeto e importa matematica.estatistica: aí matematica vira o topo, e o .. quebra de novo. A forma correta de executar um módulo que vive dentro de um pacote é python -m meu_projeto.matematica.estatistica, executada da pasta que contém meu_projeto. O -m importa pelo nome completo, preservando a hierarquia, e ainda ajusta o sys.path de forma adequada. A recomendação de fundo, porém, vai além do comando: a PEP 8 prefere importação absoluta, e a razão é exatamente esta — from meu_projeto.utils.strings import formatar é mais longa de escrever e não depende de onde o programa foi iniciado. Importação relativa é conveniente dentro de um pacote coeso e para movimentação lateral, com um ponto só; qualquer travessia que suba de nível é candidata a virar absoluta.
Exercício 2
Uma equipe mantém requirements.txt gerado com pip freeze. O arquivo tem 87 linhas, o projeto importa 6 bibliotecas, e ninguém sabe dizer quais linhas podem ser removidas. Além disso, uma atualização de segurança de uma dependência exigiu editar o arquivo à mão e quebrou a instalação. Explique a causa e descreva um fluxo melhor.
Ver resposta
✓ Resposta: O pip freeze lista tudo o que está instalado no ambiente, sem distinguir o que o projeto pediu do que veio junto. As 6 bibliotecas importadas trouxeram dezenas de dependências transitivas, e o arquivo as registra todas no mesmo nível, como se cada uma fosse uma decisão da equipe. Some-se o que alguém instalou para um teste e esqueceu de remover, e chega-se às 87 linhas que ninguém sabe interpretar — a informação de quem pediu o quê nunca foi gravada, e não há como recuperá-la lendo o arquivo. O segundo problema decorre do primeiro: editar à mão a versão de uma dependência transitiva desmonta o conjunto que o resolvedor havia calculado, porque as versões travadas foram escolhidas para serem compatíveis entre si. Subir uma sem recalcular as outras produz combinações que o pip aceita instalar e que quebram na importação. O fluxo melhor separa dois arquivos com papéis distintos. O primeiro é escrito por pessoas e lista apenas as dependências diretas, com restrições de versão frouxas — hoje o lugar dele é a seção dependencies do pyproject.toml. O segundo é gerado por ferramenta e trava todas as versões, incluindo as transitivas, para que a instalação seja idêntica em qualquer máquina; ferramentas como pip-tools, uv e Poetry produzem esse arquivo a partir do primeiro. A atualização de segurança passa então a ser feita no arquivo legível, ou por um comando de atualização, e o arquivo travado é regenerado — nunca editado. Vale acrescentar duas práticas que o cenário pede: o arquivo travado deve ser versionado junto do código, para que a instalação de hoje seja reproduzível daqui a um ano, e as dependências de desenvolvimento — teste, formatador, verificador de tipos — pertencem a um grupo separado, para não irem parar no ambiente de produção.
Exercício 3
Um desenvolvedor cria a pasta utilitarios/ com alguns módulos e esquece o __init__.py. Para surpresa dele, from utilitarios.strings import formatar funciona normalmente. Explique por quê, e diga em que situação essa mesma ausência produz um comportamento difícil de diagnosticar.
Ver resposta
✓ Resposta: Funciona porque o __init__.py deixou de ser obrigatório no Python 3.3, quando foram introduzidos os pacotes de espaço de nomes. Um diretório sem esse arquivo continua sendo importável; ele apenas passa a ser um pacote de outro tipo. A regra que muita gente aprendeu — pacote é a pasta que tem
— descreve o comportamento do Python 2 e da primeira metade do Python 3, e é a origem da surpresa. A diferença entre os dois tipos é o que cria o problema difícil de diagnosticar. Um pacote regular, com __init__.py__init__.py, mora num diretório só. Um pacote de espaço de nomes pode se espalhar por vários diretórios do sys.path, e essa é justamente a finalidade dele: permitir que partes de um mesmo pacote sejam distribuídas separadamente. A consequência colateral é que qualquer diretório com o nome procurado passa a valer como pacote, mesmo vazio. Se alguém digitar utilitarios com erro em algum lugar, ou se existir uma pasta homônima em outro ponto do sys.path — uma pasta de dados, um diretório de build, uma sobra de refatoração —, o Python encontra o nome, monta um pacote vazio e a importação do módulo interno falha com ModuleNotFoundError apontando para o submódulo, não para a pasta errada que foi encontrada primeiro. O erro descreve o sintoma, e a causa é a pasta que ninguém suspeita. Há ainda dois efeitos menores: ferramentas de descoberta de testes e de empacotamento tratam os dois tipos de forma diferente, e algumas simplesmente não encontram pacotes de espaço de nomes sem configuração extra. A recomendação prática, portanto, não mudou: crie o __init__.py, ainda que vazio, em todo pacote que não precise ser dividido entre distribuições — ele torna o pacote regular, dá um lugar para expor a interface pública, e elimina essa classe de ambiguidade.
Exercício 4
Um tutorial manda rodar python3 -m venv venv. Uma pessoa no Windows recebe Python não foi encontrado
; outra, num servidor Linux antigo, cria o ambiente e descobre depois que ele tem Python 3.6. Explique os dois casos e diga o que verificar antes de criar um ambiente virtual.
Ver resposta
✓ Resposta: Os dois casos vêm do mesmo mal-entendido: o nome do comando não é padronizado, e não diz qual versão vai responder. No Windows, a instalação oficial não cria um executável chamado python3. Existe python e existe o lançador py, que é o recomendado por permitir escolher a versão explicitamente com py -3.12 -m venv venv. Há ainda um detalhe que produz a mensagem citada: versões recentes do Windows trazem um atalho falso de python que apenas abre a loja de aplicativos, de modo que o comando existe
e não faz nada de útil. No Linux, o inverso: por anos python apontou para a versão 2, e as distribuições padronizaram python3 para a 3 — mas python3 é apenas um apelido para a versão 3 que a distribuição instalou, que num servidor antigo pode ser a 3.6, sem match, sem tomllib, sem as melhorias de mensagem de erro. O comando funcionou; a versão é que não é a esperada. O que verificar antes, então, são duas coisas, e ambas em uma linha cada: qual comando responde e qual versão ele traz, com python --version e python3 --version, ou py --list no Windows para ver todas as instaladas. Havendo várias versões, crie o ambiente a partir da que se quer, chamando-a explicitamente: python3.12 -m venv venv ou py -3.12 -m venv venv. Depois de ativar o ambiente o problema desaparece, porque dentro dele python e pip apontam para o interpretador escolhido, em qualquer sistema — e é por isso que, uma vez ativado, os comandos dos tutoriais voltam a funcionar como escritos. Vale a prática complementar de declarar a versão exigida no pyproject.toml, com requires-python, para que a instalação recuse um interpretador velho em vez de falhar de forma obscura mais adiante.
Exercício 5
Explique o que o bloco if __name__ == "__main__": faz e por que ele é necessário. Em seguida, analise por que colocar toda a lógica de um script dentro desse bloco, em vez de organizá-la em funções, atrapalha o teste e o reaproveitamento.
Ver resposta
✓ Resposta: Todo módulo tem um atributo __name__, e o Python o preenche de forma diferente conforme o caminho de entrada: quando o arquivo é executado diretamente, __name__ vale "__main__"; quando é importado, vale o nome do módulo. A comparação, portanto, responde à pergunta este arquivo está sendo rodado ou está sendo importado?
. Ela é necessária porque importar um módulo executa o corpo dele inteiro, de cima a baixo — é assim que as funções e classes passam a existir. Sem o bloco, qualquer código solto no nível do arquivo roda também na importação: um teste que importa o módulo para usar uma função dispara o script completo, uma ferramenta de documentação que inspeciona o arquivo executa o programa, e num projeto com multiprocessamento no Windows o efeito é uma recursão de processos, porque cada processo filho reimporta o módulo principal. Sobre a segunda parte: pôr toda a lógica dentro do bloco resolve o problema da importação acidental e cria outros dois. O primeiro é de teste — nada do que está ali dentro é alcançável de fora, já que o bloco só executa na execução direta, de modo que não há função nenhuma para chamar de um teste. A única forma de testar passa a ser rodar o script inteiro e inspecionar a saída, o que é lento, frágil e incapaz de exercitar casos específicos. O segundo é de reaproveitamento: quando outro programa precisar de um pedaço daquela lógica, não há pedaço a importar, e a saída costuma ser copiar o código, que é como dois trechos quase iguais passam a existir e divergir. A organização que resolve os dois é conhecida: a lógica vive em funções nomeadas, uma função main() orquestra e trata os argumentos da linha de comando, e o bloco if __name__ == "__main__": fica com uma única linha, main(). Assim o arquivo continua executável como script, cada parte é importável e testável em isolamento, e o ponto de entrada fica explícito para quem lê.