Notifications

No notifications

/Phase 4

Modules & Packages

Modules & Packages in Python 📦

A module is any .py file. A package is a directory of modules containing an __init__.py file. Python's module system lets you organize code, reuse logic, and leverage a massive ecosystem of third-party libraries.

Import Styles

import math                    # Full module import
from math import sqrt, pi      # Specific names
from os.path import join as pjoin  # Alias
import numpy as np             # Conventional alias

The __name__ Guard

if __name__ == "__main__":
    main()   # Runs only when executed directly

This prevents code from running when your module is imported by another script — essential for reusable modules.

Creating Packages

mypackage/
├── __init__.py     # Makes it a package
├── utils.py
└── core/
    ├── __init__.py
    └── engine.py

Virtual Environments & pip

CommandPurpose
python -m venv .venvCreate virtual environment
.venv/Scripts/activateActivate (Windows)
pip install requestsInstall a package
pip freeze > requirements.txtSave dependencies
pip install -r requirements.txtRestore dependencies

ModulePurpose
os / sysOS interaction, system args
math / randomMath ops, random generation
datetimeDates & times
collectionsCounter, defaultdict, deque
itertoolsCombinatoric iterators
jsonJSON encode/decode
pathlibObject-oriented file paths
functoolsHigher-order functions (lru_cache, reduce)

> Tip: Always use virtual environments per project to avoid dependency conflicts. Keep requirements.txt version-pinned for reproducibility.

On this page

Detailed Theory

As soon as your code grows past a few hundred lines, you'll want to split it across files. Modules and packages are how Python does that — plus they're how you reach for the enormous third-party ecosystem (FastAPI, NumPy, requests, anything you'll pip install).

What a Module Actually Is

A module is just a .py file. Any Python file you can run is also one you can import. When you import it, Python runs the file once, then exposes its names (functions, classes, variables) under the module's namespace.

# math_utils.py
PI = 3.14159
def square(x): return x * x

# main.py import math_utils print(math_utils.PI, math_utils.square(4))

Import Forms

import json                          # whole module
import numpy as np                    # alias
from math import pi, sqrt             # specific names
from collections import defaultdict   # one name from a package
from .helpers import clean            # relative (inside a package)

Avoid from module import * — it pollutes your namespace and makes refactors painful.

What a Package Actually Is

A package is a *folder* containing modules and an __init__.py (which can be empty):

myapp/
├── __init__.py
├── main.py
└── utils/
    ├── __init__.py
    ├── strings.py
    └── numbers.py

Now from myapp.utils.strings import slugify works.

pip & Virtual Environments

python -m venv .venv
.venv\Scripts\activate         # Windows
source .venv/bin/activate         # mac/linux

pip install requests pandas pip freeze > requirements.txt pip install -r requirements.txt

Always work inside a venv. It isolates project dependencies so projects don't poison each other. Modern alternatives: uv (fast), poetry, pipx for CLI tools.

Beginner Mistakes to Skip

1. Naming a file random.py / json.py / os.py. Shadows the standard library; import random will pick *your* file and break. 2. from x import *. Hides the source of names; trips linters and humans. 3. Forgetting __init__.py in folders you intend as packages (in some scenarios still required, especially with editable installs). 4. Installing globally. Always activate a venv first or use pipx for CLI tools. 5. Mixing pip and pip3 randomly. Use python -m pip install ... — it always uses the active interpreter. 6. Top-level side effects. Code at module top-level runs on import. Wrap scripts in if __name__ == '__main__':.

Intermediate: Import Resolution — sys.path

When you write import foo, Python checks:

1. Built-in modules baked into the interpreter (sys, builtins). 2. Each entry of sys.path, in order: - The directory of the running script (or current dir for the REPL). - PYTHONPATH env var. - The active interpreter's site-packages (where pip install puts things).

Inspect with import sys; print(sys.path). After import, the module is cached in sys.modules so further imports are free.

Intermediate: Absolute vs Relative Imports

# absolute (preferred)
from myapp.utils.strings import slugify

# relative (only inside a package) from .strings import slugify from ..config import settings

Absolute imports survive moving the file. Relative imports are concise inside large packages but fragile if you move things — prefer absolute in application code.

Intermediate: if __name__ == '__main__'

def main():
    ...

if __name__ == '__main__': main()

Lets the file work as a runnable script (python file.py) and as an importable module (from file import main) without auto-executing the body.

Intermediate: Standard-Library Power Tools

  • collections — Counter, defaultdict, deque, namedtuple, ChainMap.
  • itertools — chain, product, combinations, groupby, islice.
  • functools — lru_cache, partial, reduce, wraps.
  • pathlib, os, shutil — paths and filesystem.
  • datetime, time, zoneinfo — dates and times.
  • json, csv, re, urllib — data and text.
  • logging, argparse, subprocess — tooling.
  • statistics, math, random, decimal, fractions — numbers.
  • unittest, asyncio, typing — testing, async, type hints.
Spend an afternoon flipping through the docs — you'll skip a lot of pip installs by knowing what's already shipped.

Advanced: Designing a Package

myapp/
├── pyproject.toml         # metadata + deps + build
├── README.md
├── src/
│   └── myapp/
│       ├── __init__.py     # public API
│       ├── _internal.py    # leading _ = private
│       └── cli.py
└── tests/

In __init__.py re-export the public API and define __all__:

from .core import Engine, run
__all__ = ["Engine", "run"]

Users do from myapp import Engine; internals stay tidy. Names with leading underscores are convention for "don't import this".

Advanced: Editable Installs & pyproject.toml

For your own packages during development:

pip install -e .

With pyproject.toml:

[project]
name = "myapp"
version = "0.1.0"
dependencies = ["requests>=2.31", "pandas"]

[project.scripts] myapp = "myapp.cli:main"

Now your code is importable from anywhere in the venv, and myapp becomes a real CLI command.

Advanced: Circular Imports

If module A imports B and B imports A, you'll often hit ImportError or get half-initialised modules. Fixes:

  • Restructure: extract shared code into a third module C; have A and B both import C.
  • Lazy import: put import other *inside* the function that uses it.
  • Use TYPE_CHECKING: if typing.TYPE_CHECKING: from x import Y — imports at static-check time only.

Advanced: Lockfiles & Reproducibility

  • requirements.txt lists *what* you want; lockfiles (pip-tools, poetry.lock, uv.lock) pin *exact* versions and hashes — use them for production.
  • Commit lockfiles. Re-create environments with one command.
  • For CLIs you want available everywhere, use pipx (each tool gets its own venv).

Practice Path

1. Create a venv, install requests, write a 10-line script that fetches a URL and prints the status code, and freeze the deps to requirements.txt. 2. Split a single script.py (with helpers + main loop) into a tiny package myapp/ with utils.py and a cli.py entry point. 3. Add a pyproject.toml and pip install -e . your package; run it from any directory. 4. Demonstrate a circular import and fix it by moving the shared piece to a third module.