Skip to content
elephantoo

Modules, packages & imports

Lesson 15 of 38 14 min read

Splitting code into modules, import styles, __name__ == "__main__", sys.path, packages and relative imports.


As programs grow, one giant file becomes impossible to navigate. Python's answer is modules (single files) and packages (folders of modules). Importing lets you reuse your own code across files and tap into the enormous standard library and the hundreds of thousands of third-party packages on PyPI.

Every .py file is a module#

Create two files in the same folder:

shapes.py
"""Helpers for simple geometry."""
import math

PI_ROUNDED = 3.14


def circle_area(radius):
    return math.pi * radius ** 2


def square_area(side):
    return side * side
main.py
import shapes

print(shapes.circle_area(2))
print(shapes.square_area(3))
print(shapes.PI_ROUNDED)
print(shapes.__doc__)
Terminal
python3 main.py
Output
12.566370614359172
9
3.14
Helpers for simple geometry.

import shapes runs shapes.py once, creates a module object, and binds it to the name shapes. You access its contents with a dot: shapes.circle_area. Keeping the module prefix makes it obvious where every name comes from.

Import styles#

Python
import math                      # import the module
from math import sqrt, pi        # import specific names
from math import factorial as fact   # rename on import
import datetime as dt            # alias a module

print(math.floor(2.7), sqrt(16), round(pi, 2), fact(5))
print(dt.date(2026, 9, 30).isoformat())
Output
2 4.0 3.14 120
2026-09-30

Guidelines (from PEP 8):

  • Put imports at the top of the file, one per line, grouped: standard library, then third-party packages, then your own modules — with a blank line between groups.
  • Prefer import module or from module import name. Avoid from module import *: it pulls in unknown names and makes it hard to tell where anything came from.
  • Use well-known aliases (import numpy as np, import pandas as pd) but don't invent cryptic ones.

Modules run once#

Python caches imported modules in sys.modules. Importing the same module again, from anywhere in your program, reuses the existing object rather than re-running the file:

noisy.py
print("noisy.py is being executed")
value = 42
twice.py
import noisy
import noisy          # no second message
from noisy import value

print(value)
Output
noisy.py is being executed
42

This is also why module-level code should mostly be definitions — functions, classes, constants — not work that runs on import.

if __name__ == "__main__":#

When a file is run directly, Python sets its __name__ to "__main__"; when it's imported, __name__ is the module's name. Use that to make a file both importable and runnable:

temperature.py
def c_to_f(celsius):
    return celsius * 9 / 5 + 32


def main():
    for c in (0, 37, 100):
        print(f"{c}°C = {c_to_f(c):.1f}°F")


if __name__ == "__main__":
    main()
use_temperature.py
from temperature import c_to_f

print(c_to_f(25))       # main() did NOT run on import
Output
77.0

Running python3 temperature.py directly prints the conversion table.

How Python finds modules: sys.path#

When you import something, Python searches a list of directories stored in sys.path, in order:

  1. The directory of the script you ran (or the current directory in the REPL).
  2. Directories in the PYTHONPATH environment variable, if set.
  3. The standard library.
  4. site-packages — where pip installs third-party packages (inside your virtual environment, if one is active).
Python
import sys

for p in sys.path[:3]:
    print(p)

Because your script's folder comes first, naming a file after a standard module — random.py, json.py, email.py, test.py — silently shadows the real one. If an import behaves strangely, check for a file with a clashing name (and delete any stale __pycache__).

If a module can't be found you get:

Python
import shapez
Output
ModuleNotFoundError: No module named 'shapez'

Packages: folders of modules#

A package is a directory containing modules, normally with an __init__.py file (which may be empty). Packages can nest to any depth:

Output
store/
├── main.py
└── shop/
    ├── __init__.py
    ├── pricing.py
    └── models/
        ├── __init__.py
        └── product.py
shop/__init__.py
"""The shop package."""
from .pricing import apply_discount   # re-export for a nicer public API

__all__ = ["apply_discount"]
shop/pricing.py
def apply_discount(price, percent):
    return round(price * (1 - percent / 100), 2)
shop/models/__init__.py
shop/models/product.py
from ..pricing import apply_discount    # relative import: up one package


class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def on_sale(self, percent):
        return apply_discount(self.price, percent)
main.py
from shop import apply_discount
from shop.models.product import Product
import shop.pricing

laptop = Product("Laptop", 55000)
print(laptop.on_sale(10))
print(apply_discount(999, 50))
print(shop.pricing.apply_discount(100, 15))
Output
49500.0
499.5
85.0

Key points:

  • Dots in an import path follow folders: shop.models.product is shop/models/product.py.
  • __init__.py runs when the package is first imported. It's the place to expose a clean public API, as shop/__init__.py does above.
  • Absolute imports (from shop.pricing import ...) work from anywhere and are preferred in application code. Relative imports (from .pricing, from ..pricing) work only inside a package, and are useful within libraries.
  • __all__ lists the names that from shop import * would export — and documents the public API.

Running a module inside a package with -m#

If a module inside a package uses relative imports, running it by file path (python3 shop/models/product.py) fails with "attempted relative import with no known parent package". Run it as a module from the project root instead:

Terminal
python3 -m shop.models.product

The -m flag also runs standard-library and installed tools: python3 -m venv, python3 -m pip, python3 -m http.server, python3 -m pytest.

Exploring modules#

The REPL is the best place to poke around a module you don't know yet:

Output
>>> import math
>>> [name for name in dir(math) if name.startswith("log")]
['log', 'log10', 'log1p', 'log2']
>>> math.__name__
'math'

help(math) shows the full documentation, and math.__file__ shows where a (pure-Python) module lives on disk.

Circular imports#

If a.py imports b.py and b.py imports a.py, one of them will see a half-initialised module and you'll get an ImportError or AttributeError. Fixes: move shared code into a third module, import inside the function that needs it, or rethink the design — circular imports usually signal two modules that are too tightly coupled.

Common mistakes#

  • Naming files after standard modules (random.py, json.py, test.py).
  • Running package modules by file path — use python3 -m package.module from the project root.
  • Putting heavy work at module level — it runs on every import. Wrap it in main().
  • Forgetting that imports are cached — edits to an imported module aren't picked up in a running REPL until you restart it (or use importlib.reload).

What's next#

You can organise your own code — now let's use other people's. Next: pip and virtual environments, the essential tools for installing third-party packages safely.

Check your understanding

Quick quiz

0/3 answered
  1. 1.What is a Python module?

  2. 2.Why is from module import * discouraged?

  3. 3.You named your script random.py, and import random in it no longer gives you random.randint. Why?

Finished reading?

Mark this lesson complete to track your progress.