Nesta aula, vamos mergulhar em um dos pilares da organização de código em Python: os pacotes. Se você já trabalhou com projetos maiores, sabe que manter tudo em um único arquivo torna o código difícil de manter, testar e reutilizar. Os pacotes resolvem isso permitindo agrupar módulos relacionados em uma hierarquia de diretórios, com um namespace próprio. Vamos aprender desde o básico, como criar um pacote simples, até tópicos avançados como subpacotes e imports relativos, fundamentais para construir projetos profissionais.

Entender pacotes é essencial para distribuir bibliotecas, trabalhar em equipe e aproveitar ao máximo o ecossistema Python. Nesta aula, você verá como o arquivo __init__.py transforma um diretório comum em um pacote, como organizar código em subpacotes, e como usar imports relativos para evitar conflitos e tornar o código mais legível. Ao final, discutiremos boas práticas de estrutura de projeto, que o ajudarão a planejar seus próprios projetos desde o início.

__init__.py

O arquivo __init__.py é o marcador que indica ao Python que um diretório deve ser tratado como um pacote. Sem ele, o diretório é apenas uma pasta comum, e você não conseguirá importar módulos de dentro dele usando a sintaxe de pacote. Em versões antigas do Python (anteriores à 3.3), a presença desse arquivo era obrigatória; a partir da versão 3.3, foram introduzidos os 'namespace packages' que permitem pacotes sem __init__.py, mas ainda é uma prática altamente recomendada incluir esse arquivo para que o pacote seja tratado como um pacote regular e permita inicialização personalizada.

O __init__.py pode ser um arquivo vazio, mas geralmente contém código de inicialização do pacote, como a importação de sub-módulos para expor uma interface pública mais limpa. Por exemplo, se você tem um pacote chamado meu_pacote com um módulo modulo.py, pode importar diretamente de meu_pacote se no __init__.py você fizer from . import modulo. Isso permite que os usuários do pacote façam from meu_pacote import modulo ou até from meu_pacote import alguma_funcao se você expor funções específicas. O __init__.py também é o lugar ideal para definir a variável __all__, que controla o que é importado quando se usa from pacote import *.

Vamos criar um exemplo prático. Suponha a seguinte estrutura:

meu_pacote/
    __init__.py
    modulo.py

No arquivo __init__.py:

# __init__.py
from .modulo import funcao_principal

__all__ = ["funcao_principal"]

E no modulo.py:

# modulo.py
def funcao_principal():
    return "Olá do módulo!"

Agora, do lado de fora, você pode importar:

from meu_pacote import funcao_principal
print(funcao_principal())  # Saída: Olá do módulo!

Isso é muito conveniente, pois esconde a estrutura interna do pacote e oferece uma API limpa.

Subpacotes

Assim como você pode ter módulos dentro de um pacote, também pode ter pacotes dentro de pacotes, chamados subpacotes. Isso permite uma hierarquia mais profunda, organizando o código em categorias. Por exemplo, um projeto de processamento de imagens pode ter um pacote processamento com subpacotes filtros, transformacoes e utils. Cada subpacote é um diretório contendo seu próprio __init__.py.

Para criar um subpacote, basta criar um diretório dentro de um pacote e adicionar um __init__.py dentro dele. A importação de módulos de subpacotes segue a mesma sintaxe de pontos. Por exemplo, se você tem a estrutura:

pacote_principal/
    __init__.py
    subpacote/
        __init__.py
        modulo_secundario.py

Você pode importar o módulo de duas formas:

# Importação absoluta (recomendada)
from pacote_principal.subpacote import modulo_secundario
# ou
import pacote_principal.subpacote.modulo_secundario

E dentro do código, você pode se referir à função modulo_secundario.minha_funcao().

Subpacotes são especialmente úteis para projetos grandes, pois permitem separar claramente diferentes responsabilidades. Por exemplo, em um projeto web, você pode ter app/, app/modelos, app/controladores e app/visoes. Cada subpacote pode conter vários módulos. Isso melhora a legibilidade e a manutenção, pois cada parte do sistema fica isolada em seu próprio namespace.

Vamos ver um exemplo concreto. Crie a seguinte estrutura:

animais/
    __init__.py
    mamiferos/
        __init__.py
        cao.py
        gato.py
    aves/
        __init__.py
        passaro.py

No arquivo animais/mamiferos/cao.py:

def latir():
    return "Au au!"

No arquivo animais/aves/passaro.py:

def cantar():
    return "Piu piu!"

Agora, para usar essas funções:

from animais.mamiferos.cao import latir
from animais.aves.passaro import cantar

print(latir())
print(cantar())

Note que cada subpacote pode ter seu próprio __init__.py para expor uma interface específica, mas isso é opcional.

Imports relativos

Dentro de um pacote, você pode importar módulos usando imports absolutos (como from pacote.subpacote import modulo) ou imports relativos, que usam pontos para indicar a posição relativa do módulo atual. Imports relativos são úteis para evitar repetir o nome do pacote raiz e para facilitar a renomeação ou movimentação do pacote sem quebrar as importações internas. A sintaxe usa um ponto para o pacote atual, dois pontos para o pacote pai, etc.

Por exemplo, dentro do módulo animais/mamiferos/cao.py, se quiséssemos importar algo de gato.py (mesmo diretório), usaríamos from . import gato ou from .gato import funcao. Se quiséssemos importar algo do pacote pai (animais), usaríamos from .. import algum_modulo. Três pontos significam o avô, e assim por diante.

Vamos ilustrar com um exemplo. Considere a estrutura:

pacote/
    __init__.py
    modulo_a.py
    subpacote/
        __init__.py
        modulo_b.py

Suponha que modulo_b.py queira importar uma função de modulo_a.py. Como modulo_b está em pacote.subpacote, o pai é pacote, então o import relativo seria:

# modulo_b.py
from ..modulo_a import funcao_a

Isso é mais robusto do que usar o import absoluto from pacote.modulo_a import funcao_a, pois se você renomear o pacote raiz de pacote para meu_pacote, o import relativo continuará funcionando sem alteração.

Um ponto importante: imports relativos só funcionam dentro de módulos que fazem parte de um pacote. Se você executar um módulo diretamente como script (por exemplo, python modulo_b.py), os imports relativos falharão porque o módulo é tratado como __main__ e não tem pacote pai. Portanto, é recomendado usar imports relativos apenas em módulos que serão importados, não executados diretamente.

Exemplo prático: dentro de modulo_b.py:

# modulo_b.py
from ..modulo_a import funcao_a

def funcao_b():
    return funcao_a() + " modificado por b"

E no modulo_a.py:

# modulo_a.py
def funcao_a():
    return "Resultado de A"

Se você estiver na raiz do projeto (onde está o pacote pacote), pode testar:

from pacote.subpacote.modulo_b import funcao_b
print(funcao_b())  # Saída: Resultado de A modificado por b

Estrutura de projeto

Uma boa estrutura de projeto é fundamental para a manutenção e escalabilidade. Ao usar pacotes, você pode organizar seu código de forma lógica. Uma estrutura comum para projetos Python é:

meu_projeto/
    meu_pacote/
        __init__.py
        modulo1.py
        modulo2.py
        subpacote/
            __init__.py
            modulo3.py
    tests/
        test_modulo1.py
        test_modulo2.py
    setup.py
    README.md
    requirements.txt

O diretório raiz meu_projeto contém o pacote principal (meu_pacote), onde fica o código-fonte. Os testes ficam em um diretório separado tests. O arquivo setup.py (ou pyproject.toml) é usado para empacotar e distribuir o projeto. Arquivos como README.md e requirements.txt documentam e listam dependências.

Dentro do pacote, é comum usar subpacotes para separar diferentes funcionalidades. Por exemplo, em um projeto de análise de dados, você pode ter:

meu_projeto/
    analise/
        __init__.py
        processamento/
            __init__.py
            limpeza.py
            transformacao.py
        visualizacao/
            __init__.py
            graficos.py
        utils/
            __init__.py
            helpers.py

Essa separação ajuda na organização e evita que módulos fiquem muito grandes. Além disso, você pode usar o __init__.py de cada subpacote para expor as funções mais importantes, criando uma API pública consistente.

Outra prática recomendada é usar imports relativos dentro do pacote para evitar dependências do nome do pacote raiz. Isso torna o código mais portátil. Por exemplo, dentro de analise/processamento/limpeza.py, se precisar usar uma função de analise/utils/helpers.py, você faria:

from ..utils import helpers

Ou, se estiver no mesmo nível, from . import modulo.

Além disso, é importante pensar no que será exposto publicamente. Use o __all__ nos __init__.py para definir a interface pública. Por exemplo:

# analise/__init__.py
from .processamento import limpeza
from .visualizacao import graficos

__all__ = ["limpeza", "graficos"]

Isso permite que, ao importar o pacote, você tenha acesso direto a esses subpacotes, sem expor detalhes internos.

Boas práticas

Ao trabalhar com pacotes, algumas boas práticas ajudam a manter o código limpo e sustentável:

  • Sempre inclua __init__.py nos diretórios que deseja tratar como pacotes, mesmo que vazio. Isso evita ambiguidades com namespace packages e garante compatibilidade com ferramentas.
  • Use imports relativos dentro do pacote para reduzir acoplamento e facilitar a movimentação do código.
  • Defina o __all__ nos __init__.py para controlar a exportação pública e evitar expor módulos internos.
  • Mantenha os módulos pequenos e com responsabilidade única. Se um módulo ficar muito grande, considere dividi-lo em um subpacote.
  • Estruture os testes em um diretório separado, refletindo a estrutura do pacote, para facilitar a localização dos testes.
  • Documente a estrutura do projeto em um README, explicando o propósito de cada pacote e como importar.

Referências

Exercícios

  1. Exercício 1: Crie um pacote chamado calculadora com um módulo operacoes.py que contenha funções para somar, subtrair, multiplicar e dividir. No __init__.py, exponha essas funções diretamente. Após criar, escreva um script que importe e use as funções.
  2. ✓ Resposta:

    Estrutura:

    calculadora/
        __init__.py
        operacoes.py
    

    operacoes.py:

    def somar(a, b):
        return a + b
    
    def subtrair(a, b):
        return a - b
    
    def multiplicar(a, b):
        return a * b
    
    def dividir(a, b):
        if b == 0:
            raise ValueError("Divisão por zero")
        return a / b
    

    __init__.py:

    from .operacoes import somar, subtrair, multiplicar, dividir
    

    Script de uso:

    from calculadora import somar, dividir
    print(somar(5, 3))
    print(dividir(10, 2))
    
  3. Exercício 2: Dentro do pacote calculadora, crie um subpacote avancado com um módulo potencia.py que tenha a função potencia(base, expoente). Use import relativo no __init__.py principal para expor essa função também.
  4. ✓ Resposta:

    Estrutura:

    calculadora/
        __init__.py
        operacoes.py
        avancado/
            __init__.py
            potencia.py
    

    avancado/potencia.py:

    def potencia(base, expoente):
        return base ** expoente
    

    avancado/__init__.py:

    from .potencia import potencia
    

    __init__.py principal:

    from .operacoes import somar, subtrair, multiplicar, dividir
    from .avancado import potencia
    
  5. Exercício 3: No módulo operacoes.py, adicione uma função media(lista) que calcule a média de uma lista de números. Em seguida, dentro do mesmo pacote, crie um módulo estatisticas.py que importe somar e media de operacoes usando imports relativos. Faça com que estatisticas.py exponha uma função desvio_padrao(lista) que use essas funções.
  6. ✓ Resposta:

    Adicione em operacoes.py:

    def media(lista):
        if not lista:
            raise ValueError("Lista vazia")
        return somar(*lista) / len(lista)
    

    Crie estatisticas.py:

    from .operacoes import somar, media
    import math
    
    def desvio_padrao(lista):
        if len(lista) < 2:
            raise ValueError("Desvio padrão requer pelo menos 2 elementos")
        med = media(lista)
        soma_quadrados = somar(*[(x - med) ** 2 for x in lista])
        return math.sqrt(soma_quadrados / (len(lista) - 1))
    
  7. Exercício 4: Explique por que imports relativos não funcionam quando um módulo é executado diretamente como script. Dê um exemplo de erro que ocorreria.
  8. ✓ Resposta:

    Quando você executa um arquivo Python diretamente (ex.: python modulo.py), o Python define a variável __name__ como "__main__" e não associa o arquivo a um pacote. Imports relativos dependem do __package__ para resolver o ponto de partida. Sem pacote, o Python não sabe o que . significa, então lança um ImportError: attempted relative import with no known parent package. Por exemplo, se você tentar executar python meu_pacote/modulo.py e o módulo contém from . import outro_modulo, você verá esse erro.

  9. Exercício 5: Estruture um projeto chamado sistema_biblioteca com pacotes para modelos (definir classes Livro, Usuario), servicos (funções de empréstimo) e utils (funções de validação). Descreva a estrutura de diretórios e mostre como um módulo de servicos importaria de modelos usando import relativo.
  10. ✓ Resposta:

    Estrutura:

    sistema_biblioteca/
        __init__.py
        modelos/
            __init__.py
            livro.py
            usuario.py
        servicos/
            __init__.py
            emprestimo.py
        utils/
            __init__.py
            validacao.py
    

    Em servicos/emprestimo.py, para importar a classe Livro:

    from ..modelos.livro import Livro
    

    E para importar a função de validação:

    from ..utils.validacao import validar_cpf