Modules: Why We Split into Files, import, from and as
- Explain what a Module is and why a project is split into several files
- Create a Module of your own and import functions and constants from it
- Tell apart
import x,from x import yandimport x as z, and know when to use each - Understand that code at the top level of a Module runs at import time
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 fileprices.py→ the moduleprices. - That is why file names must be valid Python names:
snake_case, without hyphens (my-utils.pywill not import!), and not starting with a digit.
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”.
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?
- Python looks for the file: first in the folder of the script you ran, and then in Python’s libraries.
- 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.
- The result is saved. Another
importof 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
# file: prices.py (the module)
VAT_RATE = 0.17
def add_vat(amount):
return amount * (1 + VAT_RATE)# 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:
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:
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:
Base: 200
After 10% discount: 180.0
With VAT (0.17): 210.6Breaking 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:
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_LENGTHAnd update main.py:
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:
Laptop -> 4095.0 NIS incl. VAT
Skipping invalid name: 'X'
Skipping invalid price for Mouse : -5
Desk -> 1053.0 NIS incl. VATBreakdown:
import prices as p— a short nickname. You access it withp.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%rin 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:
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:
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.
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.
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):
After discount: 180.0
With VAT: 210.6Show solution
prices.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
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(...).
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?
Show solution
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.
Exercise 3: The Standard Library
Without writing a module of your own, use Python’s built-in modules:
math— the square root of 81, andmath.pirounded to two digits.random— withrandom.seed(1), draw a number between 1 and 6.
Expected output:
9.0
3.14
2Show solution
import math
import random
print(math.sqrt(81))
print(round(math.pi, 2))
random.seed(1)
print(random.randint(1, 6))9.0
3.14
2math 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.
Exercise 4: Code that runs at import time
In the file helpers.py:
def shout(text):
return text.upper() + "!"
print("helpers loaded")In the file main.py:
from helpers import shout
print(shout("hi"))- What will be printed when you run
python main.py, and why? - How do you prevent the first line?
Show solution
- This will be printed:
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.
- 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 learnif __name__ == "__main__":, which lets you keep test code that runs only when the file is run directly.)
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.
cities = ["Eilat", "Haifa", "Jerusalem"]
temperatures = [35, 22, 9]Expected output:
Eilat: 35 C / 95.0 F - hot
Haifa: 22 C / 71.6 F - pleasant
Jerusalem: 9 C / 48.2 F - coldShow solution
temperature.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
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.
Summary
- Module = a
.pyfile. 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.pyetc. - 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 *.