Lesson 10 of 16 · Level 3: Advanced
Python Modules, Packages, pip and Virtual Environments
- ⏱ 13 min read
- ❓ 4 review questions
- ✍️ Exercise with solution
In this lesson
- Modules: your code in several files
- Packages: folders of modules
- pip: installing libraries
- Virtual environments: one Python per project
- requirements.txt: make your project run on another computer
- Automated tests with pytest
- Real case: a print shop's quote calculator, with tests
- Common mistakes with modules and libraries
- Professional style
- Summary
- Split a program into several files (modules) and packages.
- Install libraries with
pipand isolate them in a virtual environment. - Share your project so it runs on someone else's computer.
- Write your first automated tests with
pytest. - Diagnose
ModuleNotFoundErrorandImportErrorin seconds.
A 50-line script fits in one file. A real project doesn't. In this lesson you'll organize code the way professional teams do and learn to manage external libraries, which are Python's real superpower: there are hundreds of thousands of them, for almost any task you can imagine. These are also the skills that separate "I wrote a script once" from "I can work on a team's codebase."
pip downloads libraries frompip install -r requirements.txt rebuilds all your dependencies on another computerModules: your code in several files
A module is simply a .py file. Anything you define in it can be imported from another file in the same folder.
# file: finance.py
SALES_TAX = 0.0825
def price_with_tax(price):
return round(price * (1 + SALES_TAX), 2)
def loan_payment(amount, apr, months):
r = apr / 12
return round(amount * r / (1 - (1 + r) ** -months), 2)
# file: main.py
import finance
from finance import loan_payment
print(finance.price_with_tax(100)) # 108.25
print(loan_payment(25_000, 0.069, 60)) # 493.85
There are three ways to import:
- You write
finance.loan_payment()- Advantage
- You always know where each name comes from.
- You write
from finance import loan_payment- Advantage
- Short names when you use just a few functions.
- You write
import pandas as pd- Advantage
- Shortens big libraries with aliases everyone recognizes.
Avoid from module import *: it pulls in everything without telling you what, and it can overwrite your own names.
What if __name__ == "__main__" really means
When you import a module, Python runs all of its code. If finance.py had a stray test print, it would show up every time you imported it. That's why "run this" code is guarded like this (you saw it in lesson 7):
if __name__ == "__main__":
print(loan_payment(5_000, 0.2, 12)) # only runs when this file is launched directly
Packages: folders of modules
When you have many modules, group them in folders. A folder with an __init__.py file (it can be empty) is a package:
expense_tracker/
├── main.py
├── requirements.txt
├── expenses/
│ ├── __init__.py
│ ├── models.py ← classes (lesson 9)
│ ├── storage.py ← read and save the CSV
│ └── reports.py ← summaries
└── tests/
└── test_reports.py
# in main.py
from expenses.models import Expense
from expenses import reports
1. You have a file utils.py with a function clean_text. How do you use it from main.py in the same folder?
Python's syntax is from module import name, without the .py extension.
pip: installing libraries
pip is Python's package installer. It downloads libraries from the official Python Package Index, PyPI:
pip install requests # install
pip install pandas==3.0.0 # a specific version (example)
pip install --upgrade pandas # upgrade
pip list # see what's installed
pip uninstall requests # uninstall
If pip isn't recognized as a command, use python -m pip install … (or python3 -m pip on macOS and Linux). This form guarantees you install for the same Python you run.
Anyone can publish to PyPI, and malicious packages with names close to popular ones exist (for example, reqeusts instead of requests). Double-check the spelling and make sure the package is well known and actively maintained.
Virtual environments: one Python per project
If you install everything "globally," sooner or later two projects will need different versions of the same library and one of them will break. A virtual environment is a folder with its own Python and its own libraries, isolated from everything else.
# 1. Create the environment (once per project, inside its folder)
python -m venv .venv
# 2. Activate it (every time you open a new terminal)
.venv\Scripts\activate # Windows
source .venv/bin/activate # macOS and Linux
# 3. Install what you need: it stays inside .venv
pip install pandas requests
# 4. Leave the environment
deactivate
While the environment is active, the terminal shows (.venv) at the start of the line. VS Code detects the .venv folder and offers to use it automatically.
- Store project pandas 2.3An older project that hasn't been upgraded yet.
- Print shop project pandas 3.0A new project on the latest version, without breaking the other one.
- System Python cleanEach environment borrows only the interpreter; libraries live in each
.venv.
2. What is a virtual environment for?
It isolates dependencies: whatever you install in one project's environment doesn't touch the others.
requirements.txt: make your project run on another computer
pip freeze > requirements.txt # save the exact list of installed libraries
pip install -r requirements.txt # on another computer, install everything at once
Share your code and requirements.txt, never the .venv folder (it's heavy and tied to your operating system). If you use Git, add .venv/ to your .gitignore file.
There are faster, more complete managers, like uv and poetry, that create the environment, install packages and pin versions with a single command. Once you're comfortable with venv and pip, they're worth a try.
Automated tests with pytest
A test is code that checks that other code works. With tests you can change your program without fear: if you break something, you'll know in seconds.
# file: test_finance.py
import pytest
from finance import price_with_tax, loan_payment
def test_price_with_tax():
assert price_with_tax(100) == 108.25
def test_loan_payment():
assert loan_payment(25_000, 0.069, 60) == 493.85
def test_zero_rate_fails():
with pytest.raises(ZeroDivisionError):
loan_payment(25_000, 0, 60)
pip install pytest
pytest # finds test_*.py files and runs every test_* function
assert checks that something is true; if it isn't, the test fails and pytest shows you what it expected and what it got. The third test documents a real edge case: a 0% APR makes the formula divide by zero, which is exactly the kind of thing tests help you notice before a customer does.
3. Which file do you share so someone else can install the same libraries as your project?
With pip install -r requirements.txt, the other person rebuilds the dependencies in their own environment.
Real case: a print shop's quote calculator, with tests
Lone Star Print Co. in Austin, Texas, bills per full thousand and charges a 25% rush fee. Here's the whole project, end to end:
- Step 1Folder and environment
mkdir printshop,cd printshop,python -m venv .venvand activate it. - Step 2Dependencies
pip install pytest, thenpip freeze > requirements.txt. - Step 3A module with the logic
quotes.py: functions only, noinput(), easy to test. - Step 4Tests
test_quotes.pywith the cases that matter to the business, run withpytest.
# file: quotes.py - Lone Star Print Co. (Austin, Texas), prices in USD
import math
PRICE_PER_THOUSAND = {"business_cards": 45.0, "flyers": 120.0, "stickers": 210.0}
RUSH_FEE = 0.25
def quote(product, quantity, rush=False):
"""Price in dollars. Billed per full thousand."""
if product not in PRICE_PER_THOUSAND:
raise ValueError(f"Unknown product: {product}")
thousands = math.ceil(quantity / 1000)
price = thousands * PRICE_PER_THOUSAND[product]
if rush:
price *= 1 + RUSH_FEE
return round(price, 2)
if __name__ == "__main__":
print(quote("business_cards", 2500)) # 135.0
print(quote("flyers", 1000, rush=True)) # 150.0
# file: test_quotes.py
import pytest
from quotes import quote
def test_bills_full_thousands():
assert quote("business_cards", 2500) == 135.0
def test_rush_fee():
assert quote("flyers", 1000, rush=True) == 150.0
def test_unknown_product():
with pytest.raises(ValueError):
quote("banners", 10)
2,500 business cards are 3 thousands (math.ceil(2.5) = 3) × $45 = $135. Running pytest -q prints 3 passed. Now imagine a coworker changes the rush fee to 0.20 without telling anyone. The next time someone runs the tests, pytest catches it:
def test_rush_fee():
> assert quote("flyers", 1000, rush=True) == 150.0
E AssertionError: assert 144.0 == 150.0
E + where 144.0 = quote('flyers', 1000, rush=True)
test_quotes.py:11: AssertionError
=========================== short test summary info ============================
FAILED test_quotes.py::test_rush_fee - AssertionError: assert 144.0 == 150.0
1 failed, 2 passed in 0.01s
How to read it: the line marked > is the one that failed; the E lines show what it got (144.0) versus what was expected (150.0) and which call produced it. The bug surfaced in seconds, before a customer got a wrong quote.
Common mistakes with modules and libraries
The most frequent one: you installed a library, but Python says it doesn't exist.
ModuleNotFoundError: No module named 'requests'
It almost always means you installed it into one Python and you're running another. For example, you installed it with the virtual environment active, and today you opened a new terminal without activating it. Activate the environment (you should see (.venv)) or install with python -m pip install requests using the same python you run your program with.
Naming your file pandas.py or requests.py.
Python would import your file instead of the library. On 3.13 and newer the error even says so: consider renaming '…/pandas.py' if it has the same name as a library you intended to import.
Misspelling what you import: from quotes import quot
Read the ImportError: it ends with Did you mean: 'quote'?
Opening a new terminal and forgetting to activate the environment.
Activate .venv every time, or let VS Code do it by selecting the environment's interpreter.
Committing the .venv folder to Git or emailing it.
Share requirements.txt and put .venv/ in .gitignore.
4. Yesterday you installed requests in your project's environment. Today you open a new terminal, run your program and get ModuleNotFoundError: No module named 'requests'. What do you do first?
The library is still installed inside .venv, but the new terminal is using the system Python. Once you activate the environment (and see (.venv)), Python finds it again.
Professional style
- PEP 8 is Python's official style guide: 4-space indentation,
snake_casefor functions and variables,CamelCasefor classes, lines that aren't too long. - Tools like
ruffandblackcheck and format your code automatically. - Use type hints (lesson 7) and docstrings on public functions.
Practice exercise: reorganize your expense tracker
- Create a project folder with a virtual environment.
- Split lesson 8's expense tracker into an
expenses/package with the modulesmodels.py(anExpensedataclass),storage.py(read and save the CSV) andreports.py(the summary by category). - Leave only the menu in
main.py. - Write at least two tests for
reports.pyand run them with pytest. - Generate
requirements.txt.
See solution
# expenses/reports.py
def total_by_category(expenses):
totals = {}
for e in expenses:
totals[e["category"]] = totals.get(e["category"], 0) + e["amount"]
return totals
# tests/test_reports.py
from expenses.reports import total_by_category
def test_total_by_category():
expenses = [{"category": "food", "amount": 10.0},
{"category": "food", "amount": 5.0},
{"category": "entertainment", "amount": 20.0}]
assert total_by_category(expenses) == {"food": 15.0, "entertainment": 20.0}
def test_empty_list():
assert total_by_category([]) == {}
Run python -m pytest from the project's root folder (where main.py lives): the python -m form adds that folder to Python's search path, so the tests can find the expenses package. Plain pytest would fail here with ModuleNotFoundError: No module named 'expenses'.
Summary
- A module is a
.pyfile; a package is a folder with__init__.py. - Install libraries with
pipinside one virtual environment per project. - Share
requirements.txt, never the.venvfolder. - Tests with pytest let you change code with confidence and catch silent rule changes.
- Most
ModuleNotFoundErrormessages mean the wrong Python or an inactive environment.
Next lesson: Data Analysis with pandas: From Excel to Python. You'll use your first big external library, the standard tool for analyzing data in Python.
Create your account in 30 seconds, mark your lessons and download your certificate at the end.