Skip to content

Modules and imports

  • Lesson 8
  • 35 min
  • Module 3: Functions and program structure
Lesson 8 of 18 · Python for Practical AI EngineeringModules and imports

What you will be able to do

Move the ticket functions into a package called ticket_cleaner, and import them into the script. Then two scripts can use the same functions, and the rest of the course adds each new part as its own module.

NotebookDo this lesson in a notebookColab · Kaggle · GitHub

The notebook has every step, task and check of this lesson, with the instructions. Its first cells make the ticket-cleaner folder as at the start of this lesson, so it works without your local project. Read the explanations here, and run the code there. It needs no install.

Colab and Kaggle open your own copy. They need a free Google or Kaggle account. Stuck? See the solution notebook.

Before you startWhat to know and to have readyAbout 35 minutes

What you need to know

What you need on your computer (only for the local route; the notebook and the boxes on this page need none of it)

Tested with Python 3.14.6 on macOS. The notebook was tested in Jupyter.

A module is a file of Python code that other code can import. Every .py file is a module. Its name is the file name without .py: the file report.py is the module report.

You already import modules that come with Python: import sys, import platform. The import statement runs the module’s file once, and then gives you its functions and variables. There are two forms:

import platform
print(platform.python_version()) # module name, dot, function name
from platform import python_version
print(python_version()) # the function name only

Both forms run the same file. The first keeps the module name in front, so the reader sees where python_version comes from. The second is shorter. Use the second form for a few names that you use often.

A package is a folder of modules with a file called __init__.py in it. The __init__.py file tells Python that the folder is a package. It can be empty. You import a module from a package with a dot: from ticket_cleaner.report import count_by_category means “in the package ticket_cleaner, the module report, the function count_by_category”.

At the end of this lesson, the project looks like this:

ticket-cleaner/
ticket-cleaner/
├── .venv/
├── count_tickets.py ← the script that you run
├── requirements.txt
├── requirements-lock.txt
└── ticket_cleaner/ ← the package: code that other code imports
├── __init__.py
└── report.py ← filter_by_status, count_by_category, average_priority

The project folder has a - in its name (ticket-cleaner), and the package has a _ (ticket_cleaner). This is on purpose. A module or package name must be a valid Python name, and a - is not allowed in a Python name.

The script keeps only the part that is specific to this one program: the data and main(). The functions move to report.py, because any program that reports on tickets can use them.

When Python runs import ticket_cleaner, it looks for it in a list of folders, in order:

  1. The folder of the script that you run. For python count_tickets.py, that is ticket-cleaner/.
  2. The folders of Python’s own modules, such as sys and csv.
  3. The packages that you installed in the active environment, such as httpx.

The first match wins. Two problems come from this order. First, the package must be in the folder of the script that you run, so you run count_tickets.py, which is next to ticket_cleaner/. Second, a file of yours with the same name as a Python module hides that module. The common mistake at the end of this lesson shows this.

An import runs every line of the module, once. A def line only defines a function, so a module that contains only functions does nothing visible when you import it. A module that has a print or a call at the top level runs it during the import.

Each module has a variable __name__. When you run a file directly, its __name__ is "__main__". When the file is imported, __name__ is the module’s name. Here is a module that prints its name, run in two ways:

ticket_cleaner/report.py (a test line at the end)
print("report.py name:", __name__)
Terminal
python ticket_cleaner/report.py
python -c "import ticket_cleaner.report"
Output
report.py name: __main__
report.py name: ticket_cleaner.report

This gives you a way to say “run this only when the file is the program, not when it is imported”:

if __name__ == "__main__":
main()

This is the main entry point. Put it at the end of every script. Then a test, or a different script, can import the file to use its functions, without running the whole program.

Run Python in your browser

What is __name__ here?

This box runs the code as the main program, as python file.py does. Change the condition to __name__ == "report", and run it. Why does main() not run now?

Amira moves the three functions into the package. She leaves the code inside each function unchanged.

ticket_cleaner/__init__.py
"""Clean support-ticket records and summarise them."""
ticket_cleaner/report.py
def filter_by_status(tickets, status="open"):
"""Return the tickets that have the given status."""
selected = []
for ticket in tickets:
if ticket["status"] == status:
selected.append(ticket)
return selected
def count_by_category(tickets):
"""Return the number of tickets in each category, sorted by category."""
counts = {}
for ticket in tickets:
category = ticket["category"]
counts[category] = counts.get(category, 0) + 1
return dict(sorted(counts.items()))
def average_priority(tickets):
"""Return the average priority, or None when there are no tickets."""
if not tickets:
return None
total = 0
for ticket in tickets:
total = total + ticket["priority"]
return round(total / len(tickets), 1)

The script now starts with one import line, and ends with the main entry point:

count_tickets.py
from ticket_cleaner.report import average_priority, count_by_category, filter_by_status
TICKETS = [
{"id": "T-1001", "status": "open", "category": "billing", "priority": 2},
{"id": "T-1002", "status": "closed", "category": "login", "priority": 3},
{"id": "T-1003", "status": "open", "category": "login", "priority": 1},
{"id": "T-1004", "status": "pending", "category": "billing", "priority": 2},
{"id": "T-1005", "status": "open", "category": "billing", "priority": 3},
{"id": "T-1006", "status": "closed", "category": "shipping", "priority": 1},
]
def main():
status = "open"
selected = filter_by_status(TICKETS, status)
print(f"Tickets with status '{status}': {len(selected)}")
if not selected:
print("No tickets to count.")
return
for category, count in count_by_category(selected).items():
print(f" {category}: {count}")
print(f"Average priority: {average_priority(selected)}")
if __name__ == "__main__":
main()

The output is the same as before. That is the test of a refactor: the structure changed, and the behaviour did not.

  1. In the ticket-cleaner folder, make the package folder:

    Terminal window
    mkdir ticket_cleaner
  2. In VS Code, make ticket_cleaner/__init__.py with the one docstring line from the worked example.

  3. Make ticket_cleaner/report.py. Move (cut, then paste) the three functions from count_tickets.py into it.

  4. In count_tickets.py, add the import line at the top. Replace the last line, main(), with the main entry point.

  5. Run the script:

    Terminal window
    python count_tickets.py
    Output
    Tickets with status 'open': 3
    billing: 2
    login: 1
    Average priority: 2.0
  6. List the package folder: ls ticket_cleaner. You see a new folder, __pycache__. Python saves a compiled copy of each imported module there, so the next import is faster. You do not edit it. If you use Git, add __pycache__/ to .gitignore.

The help desk lead wants a short report of all urgent tickets, in any status. Make a new script, urgent_report.py, in the project folder. It must:

  1. Import filter_by_status from the package. Do not copy the function.
  2. Use the same six tickets.
  3. For each status ("open", "pending", "closed"), print the status and the number of its tickets with priority 1.
  4. End with the main entry point.
Expected output
open: 1 urgent
pending: 0 urgent
closed: 1 urgent
Show one answer
urgent_report.py
from ticket_cleaner.report import filter_by_status
TICKETS = [
# the same six tickets as in count_tickets.py
]
def main():
for status in ["open", "pending", "closed"]:
selected = filter_by_status(TICKETS, status)
urgent = [ticket for ticket in selected if ticket["priority"] == 1]
print(f"{status}: {len(urgent)} urgent")
if __name__ == "__main__":
main()

The six tickets are now in two scripts. That is a copy, too. In Module 4, both scripts read the tickets from one data file instead.

Common mistake

A file name that hides a Python module

Symptom: code that worked before stops with an error about a module from Python itself. For example, you made a file called csv.py to practise:

AttributeError: module 'csv' has no attribute 'DictReader' (consider renaming '/Users/amira/projects/ticket-cleaner/csv.py' since it has the same name as the standard library module named 'csv' and prevents importing that standard library module)

Cause: Python looks for modules in the folder of your script first. Your file csv.py has the same name as Python’s csv module, so import csv finds your file instead. Your file does not have DictReader. Common names that cause this are csv.py, json.py, random.py, test.py, email.py, and the names of installed packages, such as httpx.py.

Fix:

  1. Rename your file to a name that describes your code, for example read_tickets.py.
  2. Delete the __pycache__ folder next to it, so no compiled copy of the old file is left.
  3. Run the program again.

Check your understanding

You import a module, and the terminal shows a report that you did not ask for. What is missing in that module, and where?

Show the answer

The module calls its code at the top level, so the import runs it. Put the call inside if __name__ == "__main__": at the end of the file. Then the code runs only when you run the file directly, not when another file imports it.

Check your understanding

Why is the package called ticket_cleaner and not ticket-cleaner, like the project folder?

Show the answer

import ticket-cleaner is not valid Python, because Python reads - as a minus sign. A module or package name must be a valid Python name: letters, numbers and _, not starting with a number. The project folder is never imported, so it can have a -.

Recap

  • A module is a .py file. A package is a folder of modules with an __init__.py file.
  • import module and from module import name both run the module once. Python looks in the script’s folder first, then in Python’s modules, then in the installed packages.
  • Do not name your files like Python modules or installed packages.
  • End every script with if __name__ == "__main__": main(), so that an import does not run it.
  • Run scripts from the project folder. Use python -m package.module for a module inside a package.

Next, you give each ticket a proper type with a dataclass, and add type hints that show each function’s contract on its def line.

Next: Structured records