# Modules and Packages

## 1. A Module Is Just a File

Any `.py` file is a module. Importing it runs the file top-to-bottom exactly once, and binds the result as a namespace:

```python
# math_utils.py
PI = 3.14159

def square(x):
    return x * x
```

```python
# main.py
import math_utils

print(math_utils.PI)          # 3.14159
print(math_utils.square(4))    # 16
```

`import math_utils` doesn't copy `math_utils.py`'s contents into `main.py` — it executes `math_utils.py` once, creates a module object holding everything it defined, and binds the name `math_utils` to that object. Every attribute access (`math_utils.PI`) is a lookup into that module object's namespace.

## 2. Import Variants

```python
import math_utils                       # bind the whole module object
import math_utils as mu                  # bind it under an alias
from math_utils import PI, square          # bind specific names directly into this namespace
from math_utils import square as sq          # alias a specific imported name
from math_utils import *                       # bind ALL public names — generally discouraged
```

`from math_utils import PI` copies the *current* value of `PI` into the importing module's namespace at import time — if `math_utils.PI` is reassigned later, the importing module's `PI` does **not** automatically update, because it's now an independent binding to whatever object `PI` pointed to at import time (mutable objects, like a list, are a partial exception: mutating the object in place is visible everywhere, since both names still point at the same object).

## 3. Packages — Directories of Modules

A **package** is a directory containing an `__init__.py` (marking it as a package) plus other modules or subpackages:

```
myapp/
    __init__.py
    utils.py
    models/
        __init__.py
        user.py
```

```python
from myapp.utils import helper_fn
from myapp.models.user import User
```

`__init__.py` runs once when the package is first imported — it can be empty, or it can import and re-export names from submodules so callers don't need to know the internal file layout (`from myapp.models.user import User` inside `myapp/models/__init__.py` lets callers write `from myapp.models import User` instead).

## 4. `__name__ == "__main__"`

```python
# calc.py
def add(a, b):
    return a + b

if __name__ == "__main__":
    print(add(2, 3))   # only runs when calc.py is EXECUTED directly, not when imported
```

Every module has a `__name__` attribute. When a file is run directly (`python calc.py`), Python sets `__name__` to the string `"__main__"` for that module. When the same file is *imported* from elsewhere, `__name__` is set to the module's actual name (`"calc"`) instead — this is how a file can serve as both an importable library and a standalone script without its script-only code running on every import.

## 5. Absolute vs. Relative Imports

```python
# inside myapp/models/user.py
from myapp.utils import helper_fn      # absolute — full path from the top-level package
from ..utils import helper_fn            # relative — ".." means "go up one package level"
from . import order                        # relative — "." means "this same package"
```

Absolute imports are generally preferred (unambiguous, easy to grep for, don't break if a file moves within the package as easily) — relative imports exist mainly for large packages that need to remain renameable/relocatable as a unit without rewriting every internal import.

## 6. `sys.path` and How Python Finds Modules

```python
import sys
print(sys.path)   # a list of directories Python searches, in order, for modules to import
```

`sys.path` typically includes: the directory of the script being run (or the current directory, for `-c`/REPL), the `PYTHONPATH` environment variable's directories, and the standard library plus installed third-party packages' locations. `import foo` searches these directories, in order, for a match — the first `foo.py` or `foo/` package found wins, which is exactly why a local file accidentally named `random.py` can shadow the standard library's `random` module for that script.

## 7. `__all__` and Controlling `from module import *`

```python
# mymodule.py
__all__ = ["public_fn"]

def public_fn(): ...
def _private_helper(): ...   # leading underscore signals "internal" by convention
```

```python
from mymodule import *
# only public_fn is imported — _private_helper is excluded by leading underscore
# convention AND by __all__ explicitly restricting the wildcard import's scope
```

`__all__` is a list of strings defining exactly what `from module import *` exports; without it, `import *` exports every name not starting with an underscore. Neither `__all__` nor a leading underscore actually makes anything "private" in an enforced sense — both are conventions, not access control (see Under the Hood).

## 8. Under the Hood: `sys.modules`, One-Time Execution, and Circular Imports

![An import first checks sys.modules; on a miss, finders search sys.path, then a new module object is inserted into sys.modules before its top-level code executes once](diagrams/01-import-resolution.svg)

### Every module is executed exactly once per process, then cached

The first time anything imports a module, Python runs that file's top-level code and stores the resulting module object in the dict `sys.modules`, keyed by the module's fully qualified name. **Every subsequent `import` of that same module — anywhere else in the program — is just a dictionary lookup into `sys.modules`, not a re-execution of the file.**

```python
import sys

import math_utils
print("math_utils" in sys.modules)   # True

import math_utils   # does NOT re-run math_utils.py's top-level code a second time
```

This is why side effects at module level (print statements, opening a file, starting a connection) run only once no matter how many places `import` that module, and it's the exact mechanism behind why mutating a module-level variable from one importer is visible from every other importer — they all hold a reference to the *same* cached module object in `sys.modules`, not separate copies.

### Circular imports fail because of *partial* module objects, not a hard prohibition

```python
# a.py
import b
def f(): return b.g()

# b.py
import a
def g(): return a.f()
```

When `a.py` runs `import b`, Python creates a (still-empty) module object for `a` in `sys.modules` *before* executing `a.py`'s body — this is what allows `b.py`'s `import a` to succeed at all instead of infinite-looping. But if `b.py` tries to access `a.f` **at import time** (module level, not inside a function body), it fails with `AttributeError`, because `a`'s module object exists in `sys.modules` but is still empty — `a.py` hasn't gotten past its own `import b` line yet to define `f`. This is exactly why the functions above work (they only look up `b.g`/`a.f` when *called*, by which point both modules have finished executing) while equivalent module-level code (`X = b.g()` written directly in `a.py`, outside any function) would fail.

### `__all__` and the underscore convention are cosmetic, not enforced

Nothing prevents `from mymodule import _private_helper` — a leading underscore only excludes a name from a *wildcard* import (`import *`); it's fully importable and callable by explicit name. `__all__` is read by the `import *` machinery specifically, and by some documentation tools — it is not consulted by `import module_name` or `from module import specific_name` at all. Python's privacy model at the module level is a social convention enforced by linters and code review, not by the interpreter.

## 9. Common Pitfalls

1. **Naming a local file the same as a standard-library or third-party module** (`random.py`, `json.py`, `queue.py`) — because the script's own directory is searched first, this silently shadows the real module, producing confusing `AttributeError`s for functions that "should" exist.
2. **Circular imports that access module-level attributes of each other before both finish loading** — as shown in Under the Hood, this fails specifically for top-level access, not calls made later inside functions; restructuring to import inside a function body (a "lazy import") is a common, valid workaround.
3. **Assuming `from module import name` stays in sync if `module.name` is reassigned later** — it copies the reference at import time; only mutating the *same* mutable object (not reassigning the name) stays visible across both bindings.
4. **Using `from module import *` in application code** — it pollutes the namespace unpredictably (later imports can silently shadow earlier ones) and defeats static analysis tools' ability to tell where a name came from; reserve it for very small, well-controlled contexts (or avoid entirely).
5. **Putting expensive or side-effecting code at module level** — since a module only executes once and its top level runs immediately on first import, module-level network calls, file I/O, or heavy computation run at import time, which can slow down or break unrelated code that merely needs one function from the module.
6. **Forgetting `if __name__ == "__main__":`**, so a script's demonstration/test code runs every time the file is imported as a library elsewhere, not just when run directly.

## 10. Quick Reference

```python
import module                      # bind the whole module object
import module as alias               # bind under a different name
from module import name               # copy a specific name's CURRENT value into this namespace
from module import *                    # import all public names — avoid in real code
from . import sibling                    # relative import, same package
from .. import cousin_package               # relative import, one level up

if __name__ == "__main__":                    # True only when run directly, not when imported
    main()

sys.modules                                     # cache: {module_name: module_object}, populated once
sys.path                                          # search path for import, checked in order

__all__ = ["public_fn"]                             # controls `from module import *` only
```
