UNB/ CS/ David Bremner/ teaching/ cs2613/ books/ practical-python/ 09 Packages/ 01 Packages

Contents | Previous (8.3 Debugging) | Next (9.2 Third Party Packages)

9.1 Packages

If writing a larger program, you don't really want to organize it as a large of collection of standalone files at the top level. This section introduces the concept of a package.

Modules

Any Python source file is a module.

# foo.py
def grok(a):
    ...
def spam(b):
    ...

An import statement loads and executes a module.

# program.py
import foo

a = foo.grok(2)
b = foo.spam('Hello')
...

Packages vs Modules

For larger collections of code, it is common to organize modules into a package.

# From this
pcost.py
report.py
fileparse.py

# To this
porty/
    __init__.py
    pcost.py
    report.py
    fileparse.py

You pick a name and make a top-level directory. porty in the example above (clearly picking this name is the most important first step).

Add an __init__.py file to the directory. It may be empty.

Put your source files into the directory.

Using a Package

A package serves as a namespace for imports.

This means that there are now multilevel imports.

import porty.report
port = porty.report.read_portfolio('port.csv')

There are other variations of import statements.

from porty import report
port = report.read_portfolio('portfolio.csv')

from porty.report import read_portfolio
port = read_portfolio('portfolio.csv')

Two problems

There are two main problems with this approach.

So, basically everything breaks. But, other than that, it works.

Problem: Imports

Imports between files in the same package must now include the package name in the import. Remember the structure.

porty/
    __init__.py
    pcost.py
    report.py
    fileparse.py

Modified import example.

# report.py
from porty import fileparse

def read_portfolio(filename):
    return fileparse.parse_csv(...)

All imports are absolute, not relative.

# report.py
import fileparse    # BREAKS. fileparse not found

...

Relative Imports

Instead of directly using the package name, you can use . to refer to the current package.

# report.py
from . import fileparse

def read_portfolio(filename):
    return fileparse.parse_csv(...)

Syntax:

from . import modname

This makes it easy to rename the package.

Problem: Main Scripts

Running a package submodule as a main script breaks.

bash $ python porty/pcost.py # BREAKS
...

Reason: You are running Python on a single file and Python doesn't see the rest of the package structure correctly (sys.path is wrong).

All imports break. To fix this, you need to run your program in a different way, using the -m option.

bash $ python -m porty.pcost # WORKS
...

__init__.py files

The primary purpose of these files is to stitch modules together.

Example: consolidating functions

# porty/__init__.py
from .pcost import portfolio_cost
from .report import portfolio_report

This makes names appear at the top-level when importing.

from porty import portfolio_cost
portfolio_cost('portfolio.csv')

Instead of using the multilevel imports.

from porty import pcost
pcost.portfolio_cost('portfolio.csv')

Another solution for scripts

As noted, you now need to use -m package.module to run scripts within your package.

bash % python3 -m porty.pcost portfolio.csv

There is another alternative: Write a new top-level script.

#!/usr/bin/env python3
# pcost.py
import porty.pcost
import sys
porty.pcost.main(sys.argv)

This script lives outside the package. For example, looking at the directory structure:

pcost.py       # top-level-script
porty/         # package directory
    __init__.py
    pcost.py
    ...

Application Structure

Code organization and file structure is key to the maintainability of an application.

There is no "one-size fits all" approach for Python. However, one structure that works for a lot of problems is something like this.

porty-app/
  README.txt
  script.py         # SCRIPT
  porty/
    # LIBRARY CODE
    __init__.py
    pcost.py
    report.py
    fileparse.py

The top-level porty-app is a container for everything else--documentation, top-level scripts, examples, etc.

Again, top-level scripts (if any) need to exist outside the code package. One level up.

#!/usr/bin/env python3
# porty-app/script.py
import sys
import porty

porty.report.main(sys.argv)

Exercises

At this point, you have a directory with several programs:

pcost.py          # computes portfolio cost
report.py         # Makes a report
ticker.py         # Produce a real-time stock ticker

There are a variety of supporting modules with other functionality:

stock.py          # Stock class
portfolio.py      # Portfolio class
fileparse.py      # CSV parsing
tableformat.py    # Formatted tables
follow.py         # Follow a log file
typedproperty.py  # Typed class properties

In this exercise, we're going to clean up the code and put it into a common package.

Exercise 9.1: Making a simple package

Make a directory called porty/ and put all of the above Python files into it. Additionally create an empty __init__.py file and put it in the directory. You should have a directory of files like this:

porty/
    __init__.py
    fileparse.py
    follow.py
    pcost.py
    portfolio.py
    report.py
    stock.py
    tableformat.py
    ticker.py
    typedproperty.py

Remove the file __pycache__ that's sitting in your directory. This contains pre-compiled Python modules from before. We want to start fresh.

Try importing some of package modules:

>>> import porty.report
>>> import porty.pcost
>>> import porty.ticker

If these imports fail, go into the appropriate file and fix the module imports to include a package-relative import. For example, a statement such as import fileparse might change to the following:

# report.py
from . import fileparse
...

If you have a statement such as from fileparse import parse_csv, change the code to the following:

# report.py
from .fileparse import parse_csv
...

Exercise 9.2: Making an application directory

Putting all of your code into a "package" isn't often enough for an application. Sometimes there are supporting files, documentation, scripts, and other things. These files need to exist OUTSIDE of the porty/ directory you made above.

Create a new directory called porty-app. Move the porty directory you created in Exercise 9.1 into that directory. Copy the Data/portfolio.csv and Data/prices.csv test files into this directory. Additionally create a README.txt file with some information about yourself. Your code should now be organized as follows:

porty-app/
    portfolio.csv
    prices.csv
    README.txt
    porty/
        __init__.py
        fileparse.py
        follow.py
        pcost.py
        portfolio.py
        report.py
        stock.py
        tableformat.py
        ticker.py
        typedproperty.py

To run your code, you need to make sure you are working in the top-level porty-app/ directory. For example, from the terminal:

shell % cd porty-app
shell % python3
>>> import porty.report
>>>

Try running some of your prior scripts as a main program:

shell % cd porty-app
shell % python3 -m porty.report portfolio.csv prices.csv txt
      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84

shell %

Exercise 9.3: Top-level Scripts

Using the python -m command is often a bit weird. You may want to write a top level script that simply deals with the oddities of packages. Create a script print-report.py that produces the above report:

#!/usr/bin/env python3
# print-report.py
import sys
from porty.report import main
main(sys.argv)

Put this script in the top-level porty-app/ directory. Make sure you can run it in that location:

shell % cd porty-app
shell % python3 print-report.py portfolio.csv prices.csv txt
      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84

shell %

Your final code should now be structured something like this:

porty-app/
    portfolio.csv
    prices.csv
    print-report.py
    README.txt
    porty/
        __init__.py
        fileparse.py
        follow.py
        pcost.py
        portfolio.py
        report.py
        stock.py
        tableformat.py
        ticker.py
        typedproperty.py

Contents | Previous (8.3 Debugging) | Next (9.2 Third Party Packages)