# Modules: Why We Split into Files, import, from and as<no value>

## What will we learn?

- What a **Module** is.
- Why a project is split into **several files**.
- Three forms of importing: `import`, `from ... import`, `as`.
- What **exactly** happens when Python imports a file.

## Why do we need this?

We already used modules: `import os`, `import logging`, `import socket`. Today we will write **our own**.

A real project is hundreds and thousands of lines. One huge file is:

- **Hard to navigate** — where is the function that sends an email? Line 1,840?
- **Hard to work on in a team** — three developers edit the same file and run into each other (in Git, in the Git topic).
- **Hard to reuse** — a good validation function is "stuck" inside a file of a game.

Splitting into files is **Separation of Concerns at the project level**. In the backend and databases topics the server will be split exactly like this: `main.py` (Routes), `db.py` (database access), `models.py`.

## Explanation from the basics

### What is a Module?

**Module = any `.py` file.** That's it. Every Python file you wrote is already a module, and you can import functions, constants and variables from it into another file.

- The module's name = the file's name **without** `.py`. The file `prices.py` → the module `prices`.
- That is why file names must be valid Python names: `snake_case`, without hyphens (`my-utils.py` will not import!), and not starting with a digit.

{{< box "analogy" "Analogy: a kitchen with drawers" >}}
One huge file = one drawer with all the tools: knives, spoons, tongs, scissors. To find something, you search through everything. **Modules = separate drawers** with labels: "cutlery", "baking tools", "spices". `import` = "bring me something from this drawer".
{{< /box >}}

### Three forms of importing

| Form | How you use it afterwards | When |
|---|---|---|
| `import prices` | `prices.add_vat(100)` | The default. It is clear where every name comes from. |
| `from prices import add_vat` | `add_vat(100)` | When you use a few specific names a lot. |
| `import prices as p` / `from prices import add_vat as vat` | `p.add_vat(100)` / `vat(100)` | A long name or a name collision. Well-known conventions: `import numpy as np`. |

### What happens at `import` time?

1. Python **looks for** the file: first in the folder of the script you ran, and then in Python's libraries.
2. Python **runs the whole file from top to bottom**, once: it creates the functions and the constants, and **runs any code at the top level**.
3. The result is saved. Another `import` of the same module **does not run it again**.

Point 2 is very important, and we will see its consequences in the common mistake.

### `__pycache__`

After the first import, a folder named `__pycache__` with `.pyc` files will appear. This is the **Bytecode** (remember the topic on the computer and the operating system!) of the modules, saved so that the next import will be faster. Do not touch it, and in Git projects ignore it (we will see it in `.gitignore` in the Git topic).

## Syntax

```python
# file: prices.py   (the module)
VAT_RATE = 0.17

def add_vat(amount):
    return amount * (1 + VAT_RATE)
```

```python
# file: main.py     (uses the module; same folder)
import prices
prices.add_vat(100)

from prices import add_vat, VAT_RATE
add_vat(100)

import prices as p
p.add_vat(100)

from prices import add_vat as with_vat
with_vat(100)
```

**The accepted order at the top of a file:** first imports from the standard library (`os`, `json`), an empty line, and then imports from your own modules.

## First code example

Create a folder `week-2/shop/`, with two files in it.

`week-2/shop/prices.py`:

```python
VAT_RATE = 0.17

def add_vat(amount):
    return round(amount * (1 + VAT_RATE), 2)

def apply_discount(amount, percent):
    return round(amount - amount * percent / 100, 2)
```

`week-2/shop/main.py`:

```python
import prices

base_price = 200
discounted = prices.apply_discount(base_price, 10)
final_price = prices.add_vat(discounted)

print("Base:", base_price)
print("After 10% discount:", discounted)
print("With VAT (" + str(prices.VAT_RATE) + "):", final_price)
```

**Run** (from `week-2/shop`): `python main.py`

**Output:**

```text
Base: 200
After 10% discount: 180.0
With VAT (0.17): 210.6
```

## Breaking down the code line by line

| Line | Explanation |
|---|---|
| `import prices` | Python looks for `prices.py` **in the same folder** as `main.py`, runs it, and creates the name `prices` that represents the module. |
| `prices.apply_discount(...)` | You access the function **through** the module's name, with a dot. Exactly like `os.getcwd()`. |
| `prices.VAT_RATE` | Constants are accessible the same way. |
| `main.py` does not define any price function | All the price logic is in **one place**. If the VAT changes, you change only `prices.py`. |

## Another example

**The three forms of importing, and a second module.** Add `week-2/shop/validation.py`:

```python
MIN_NAME_LENGTH = 2

def is_valid_price(price):
    return isinstance(price, (int, float)) and price >= 0

def is_valid_name(name):
    return len(name.strip()) >= MIN_NAME_LENGTH
```

And update `main.py`:

```python
import prices as p
from validation import is_valid_price, is_valid_name

products = [("Laptop", 3500), ("X", 20), ("Mouse", -5), ("Desk", 900)]

for name, price in products:
    if not is_valid_name(name):
        print("Skipping invalid name:", repr(name))
    elif not is_valid_price(price):
        print("Skipping invalid price for", name, ":", price)
    else:
        print(name, "->", p.add_vat(price), "NIS incl. VAT")
```

**Output:**

```text
Laptop -> 4095.0 NIS incl. VAT
Skipping invalid name: 'X'
Skipping invalid price for Mouse : -5
Desk -> 1053.0 NIS incl. VAT
```

**Breakdown:**

- `import prices as p` — a short nickname. You access it with `p.add_vat`.
- `from validation import is_valid_price, is_valid_name` — imports **only** the two functions, and uses them directly, without a prefix.
- `repr(name)` — shows the value with quotes (`'X'`), like `%r` in logs.
- **The project structure:** `prices.py` = price logic. `validation.py` = validity rules. `main.py` = connects and runs. Every file has one responsibility.

## Common mistake

**Mistake 1: "test" code at the top level of a module.** Suppose that at the bottom of `prices.py` you added a test:

```python
print("Testing prices module:", add_vat(100))
```

Now **every** `import prices`, from any file, will print this line, because importing **runs the file**. In the next lesson we will learn the solution: `if __name__ == "__main__":`.

**Mistake 2: a file name that collides with a standard module.**

You created a file named `random.py` (or `logging.py`, `json.py`), and in another file you wrote `import random`:

```text
AttributeError: module 'random' has no attribute 'randint'
```

Python finds **your** `random.py` **first**, in the same folder, and imports it instead of the real module. Never name files with the names of standard modules.

**Mistake 3: `ModuleNotFoundError`.**

```text
ModuleNotFoundError: No module named 'prices'
```

The common reasons: the file is not in the same folder as the script you ran, a typo in the name, or a file name with a hyphen (`my-prices.py`).

**Mistake 4: `from module import *`.** Imports **all** the names from the module. It is not clear where each name came from, and names may silently overwrite each other. Import explicitly what you need.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Practice

The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.

### Exercise 1: The first module

Create a file `prices.py` with the constant `VAT_RATE = 0.17` and two functions: `add_vat(amount)` and `apply_discount(amount, percent)` (both round to two digits). In the file `main.py`, import the module, calculate a price of 200 after a 10% discount, and then add VAT to it.

**Expected output (running main.py):**

```text
After discount: 180.0
With VAT: 210.6
```

{{< solution >}}
**prices.py**

```py
VAT_RATE = 0.17


def add_vat(amount):
    return round(amount * (1 + VAT_RATE), 2)


def apply_discount(amount, percent):
    return round(amount - amount * percent / 100, 2)
```

**main.py**

```py
import prices

discounted = prices.apply_discount(200, 10)
final_price = prices.add_vat(discounted)
print("After discount:", discounted)
print("With VAT:", final_price)
```

`import prices` loads the file `prices.py` that is in the same folder. You access its content with a dot: `prices.add_vat(...)`.
{{< /solution >}}

### Exercise 2: Three forms of importing

Write the same line — calculating VAT on 100 — in the three forms of importing: `import`, `from ... import` and `import ... as`. When is it worth using each one?

{{< solution >}}
```py
import prices
print(prices.add_vat(100))

from prices import add_vat
print(add_vat(100))

import prices as p
print(p.add_vat(100))
```

- **`import prices`** — it is always clear where every name comes from (`prices.add_vat`). Suits cases where you use many things from the module.
- **`from prices import add_vat`** — you write less. Suits a few specific names you use a lot.
- **`import prices as p`** — a short name for a module with a long name.
{{< /solution >}}

### Exercise 3: The Standard Library

Without writing a module of your own, use Python's built-in modules:

1. `math` — the square root of 81, and `math.pi` rounded to two digits.
2. `random` — with `random.seed(1)`, draw a number between 1 and 6.

**Expected output:**

```text
9.0
3.14
2
```

{{< solution >}}
```python
import math
import random

print(math.sqrt(81))
print(round(math.pi, 2))

random.seed(1)
print(random.randint(1, 6))
```

```text
9.0
3.14
2
```

`math` and `random` belong to the **Standard Library** — they come with Python, and you import them exactly like a module we wrote ourselves. `random.seed(1)` fixes the draw, so that the result is the same on every run.
{{< /solution >}}

### Exercise 4: Code that runs at import time

In the file `helpers.py`:

```text
def shout(text):
    return text.upper() + "!"

print("helpers loaded")
```

In the file `main.py`:

```text
from helpers import shout
print(shout("hi"))
```

1. What will be printed when you run `python main.py`, and why?
2. How do you prevent the first line?

{{< solution >}}
1. This will be printed:

```text
helpers loaded
HI!
```

**`import` runs the whole file** — including a `print` that is at the top level. Even `from helpers import shout` runs all of `helpers.py`, not only the function.

2. A module meant to be imported should not have code at the top level — only definitions. Delete the `print`. (In the next lesson we will learn `if __name__ == "__main__":`, which lets you keep test code that runs only when the file is run directly.)
{{< /solution >}}

### Exercise 5: A temperature module

Create a module `temperature.py` with `c_to_f(celsius)` and with `describe(celsius)` that returns `"hot"` from 30 degrees, `"cold"` below 15, and `"pleasant"` in between. In `main.py`, print for every city the temperature in both units and the description.

```py
cities = ["Eilat", "Haifa", "Jerusalem"]
temperatures = [35, 22, 9]
```

**Expected output:**

```text
Eilat: 35 C / 95.0 F - hot
Haifa: 22 C / 71.6 F - pleasant
Jerusalem: 9 C / 48.2 F - cold
```

{{< solution >}}
**temperature.py**

```py
HOT_FROM = 30
COLD_BELOW = 15


def c_to_f(celsius):
    return round(celsius * 9 / 5 + 32, 1)


def describe(celsius):
    if celsius >= HOT_FROM:
        return "hot"
    if celsius < COLD_BELOW:
        return "cold"
    return "pleasant"
```

**main.py**

```py
from temperature import c_to_f, describe

cities = ["Eilat", "Haifa", "Jerusalem"]
temperatures = [35, 22, 9]

for i in range(len(cities)):
    celsius = temperatures[i]
    print(cities[i] + ":", celsius, "C /", c_to_f(celsius), "F -", describe(celsius))
```

The temperature logic is in the module, and `main.py` only uses it and prints.
{{< /solution >}}

<!-- exercises:end -->

## Summary

- **Module = a `.py` file.** The module's name = the file's name without `.py` (`snake_case`, no hyphens).
- `import m` → `m.func()`. `from m import func` → `func()`. `import m as x` → `x.func()`.
- Python looks for modules first **in the script's folder**. Do not name files `random.py`, `json.py` etc.
- Importing **runs the module** once. Code at the top level runs even when importing.
- Splitting into files = Separation of Concerns at the project level. No `import *`.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Official documentation

- [Python Tutorial — Modules](https://docs.python.org/3/tutorial/modules.html)
- [More on Modules](https://docs.python.org/3/tutorial/modules.html#more-on-modules)

<!-- docs:end -->
