Modules, Packages & Virtual Environments
import, pip, requirements.txt, and virtual environments — how real Python projects are actually structured.
Every .py File Is a Module
A module is nothing more mysterious than a single .py file. The moment you write import math, Python goes and finds a file called math (in this case, a built-in one written in C, but the concept is identical for your own files) and runs it top to bottom exactly once, then exposes everything it defined — functions, classes, variables — as attributes you can reach through the module name.
# helpers.py
def greet(name):
return f"Hello, {name}!"
PI_ISH = 3.14# main.py, in the same folder as helpers.py
import helpers
print(helpers.greet("Asha")) # "Hello, Asha!"
print(helpers.PI_ISH) # 3.14Three import forms cover almost everything you will write. import helpers gives you the whole module under its own name, so you always write helpers.greet(...) — this is the form recommended by most style guides because it is unambiguous about where greet came from when someone reads the code later. from helpers import greet pulls a specific name directly into your file's namespace, letting you call greet(...) with no prefix — convenient, but it gets harder to trace where a name came from once a file has a dozen such imports. import helpers as h aliases the module to a shorter name — extremely common for libraries with conventional aliases, like import pandas as pd.
from module import * in real code. It dumps every public name from the module into your file's namespace with no indication of where any of them came from, and it can silently shadow names you already defined. Every linter flags it, and it is one of the fastest ways to make a codebase impossible to navigate.A module only runs once per program, no matter how many times it is imported
Python caches every module it imports in sys.modules the first time it is imported. If five different files each write import helpers, the helpers.py file's top-level code runs exactly once — the first import — and every subsequent import just hands back a reference to that same already-built module object. This is why placing code with side effects (like a database connection, or a print() call) at the top level of a module is a common source of subtle bugs: it only fires once, at first import, not once per file that imports it.
A Package Is a Folder of Modules
A package is simply a folder containing Python files, plus (traditionally) a special file called __init__.py that marks the folder as importable. Packages let you organise related modules into a namespace instead of dumping every file flat into one directory.
myapp/
__init__.py
database.py
utils/
__init__.py
formatting.py
validation.pyfrom myapp.utils.formatting import format_currency
from myapp import database
format_currency(1999) # "$19.99"
database.connect()__init__.py can be a completely empty file — its mere presence used to be what told older Python versions "this folder is a package, not just a folder." Modern Python (3.3+) supports namespace packages, which work even without an __init__.py, but nearly every real project still includes one deliberately, because it is also the natural place to control what a package exposes — re-exporting selected names from its submodules so callers can write from myapp import connect instead of reaching two levels deep.
The module vs. package distinction, in one sentence
A module is a file; a package is a folder of modules (which may itself contain sub-packages, nested arbitrarily deep) — both are imported the same way, with dots marking each level of nesting.
sys.path — The Search Order Behind Every import
When you write import helpers, Python does not search your whole filesystem — it checks a specific, ordered list of locations stored in sys.path, and uses the first match it finds.
import sys
for p in sys.path:
print(p)
# Typically, in this order:
# 1. The directory of the script being run (or '' for the interactive interpreter)
# 2. PYTHONPATH environment variable entries, if set
# 3. The standard library's installation directories
# 4. site-packages — where pip installs third-party packagesThis ordering explains a genuinely common beginner bug: naming your own file random.py or json.py in the same folder as your script. Because "the directory of the script being run" is searched first, your file shadows the real standard-library module of the same name — any import random anywhere in your program now finds your file instead of Python's actual random module, usually producing a confusing AttributeError deep inside unrelated code.
AttributeError: module 'random' has no attribute 'randint' and you are certain you never touched the real random module, check whether you have a file called random.py anywhere in your project — it is shadowing the real one.Two Ways to Reference a Sibling Module
Inside a package, you can import a sibling module either absolutely (spelling out the full path from the top of the package) or relatively (using dots to mean "relative to my own location").
# myapp/utils/validation.py
from myapp.utils.formatting import format_currency# myapp/utils/validation.py
from .formatting import format_currency # . = "the same package as this file"
from ..database import connect # .. = "one level up"PEP 8 and most real codebases prefer absolute imports for their clarity — reading from myapp.utils.formatting import format_currency tells you exactly where the name lives without needing to know the importing file's own location. Relative imports earn their keep mainly inside large packages that get renamed or moved as a unit, where absolute paths would need updating everywhere; for everyday project code, absolute imports are the safer default.
Relative imports only work inside a package — never in a directly-run script
# If validation.py (which contains "from .formatting import ...") is run directly:
$ python myapp/utils/validation.py
ImportError: attempted relative import with no known parent package
# Relative imports require the file to be run as PART OF a package, e.g.:
$ python -m myapp.utils.validationThe Idiom That Separates “Importable” From “Runnable”
Every Python file has a built-in variable called __name__. When a file is run directly (python script.py), Python sets its __name__ to the string "__main__". When that same file is instead imported by another file, __name__ is set to the module's actual name instead. This single difference is what the near-universal if __name__ == "__main__": guard is built on.
# analysis.py
def calculate_average(numbers):
return sum(numbers) / len(numbers)
def run_demo():
print(calculate_average([10, 20, 30]))
if __name__ == "__main__":
run_demo()
# Run directly: python analysis.py -> prints 20.0
# Imported elsewhere: import analysis -> nothing prints;
# calculate_average is available, run_demo() never firesWithout the guard, run_demo() would execute every time anything imported analysis.py — including test files, other modules that just want calculate_average, and tools that import your code to inspect it. The guard is what lets a single file be both a reusable module and a standalone script, cleanly.
Installing Code Someone Else Already Wrote
The vast majority of real Python projects depend on third-party packages — code published to PyPI (the Python Package Index) and installed with pip, Python's standard package manager.
pip install requests # install the latest version
pip install requests==2.31.0 # install an exact, pinned version
pip install "requests>=2.28,<3" # install within a version range
pip uninstall requests # remove it
pip list # show everything currently installed
pip show requests # details about one installed packagerequirements.txt — recording exactly what your project needs
A requirements.txt file lists a project's dependencies (and usually their exact versions) in one place, so anyone else — a teammate, a CI server, a production deployment — can recreate the same environment with a single command.
requests==2.31.0
pandas==2.1.4
python-dateutil==2.8.2pip freeze > requirements.txt # capture everything currently installed
pip install -r requirements.txt # install everything a requirements.txt listspip freeze captures every package in the current environment — including things you never directly installed but that came along as a dependency of something else. Larger projects increasingly use tools like pip-tools or poetry to separate "what I directly depend on" from "the full resolved dependency tree," but plain requirements.txt remains extremely common and is a completely reasonable default for most projects.Why Every Real Project Needs Its Own Isolated Environment
Without a virtual environment, pip install installs packages globally — into the one shared Python installation on your machine. That sounds convenient until you have two projects that need different, incompatible versions of the same package: Project A needs django==3.2, Project B needs django==5.0, and a single global installation can only hold one version at a time. A virtual environment (venv) is a self-contained, isolated Python installation per project, so each project's dependencies never collide with any other's.
# Create one, typically named .venv, inside your project folder
python3 -m venv .venv
# Activate it — this changes which "python" and "pip" your terminal uses
source .venv/bin/activate # macOS / Linux
.venv\Scripts\activate # Windows
# Your prompt now shows (.venv) — everything installed from here
# goes into THIS project's isolated environment, not the global one
pip install requests
# Leave the virtual environment
deactivatepip install without activating the venv first. The package installs successfully — just into the wrong place (the global environment, or a completely different project's venv if one happens to still be active) — and the current project's script then fails with ModuleNotFoundError even though "it was just installed." Always confirm the venv is active (check for the (.venv) prefix in your terminal prompt) before installing anything..venv should never be committed to version control
A virtual environment folder can be tens or hundreds of megabytes and is entirely machine-specific — it should always be listed in .gitignore. What gets committed instead is requirements.txt, which lets anyone recreate an equivalent environment from scratch with python3 -m venv .venv && pip install -r requirements.txt.
Onboarding at a Seattle Fintech Startup
A new hire clones the payments-service repository and, following old habits, runs pip install -r requirements.txt directly — no venv. It appears to work. Two hours later, running the test suite fails with a version conflict: the globally installed cryptography package (pulled in months ago by an unrelated personal project) is newer than the version this repository pins, and the two are incompatible in a way that produces a cryptic import error deep inside a third-party library, nothing pointing at the real cause.
# Undo the global install's damage isn't really possible cleanly —
# start fresh with a proper isolated environment instead:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# Now "cryptography" resolves to EXACTLY the version requirements.txt
# pins, isolated from anything else ever installed on this machineWhy this is standard, not optional, at every real engineering org
The senior engineer's message in the team channel afterward: "every repo gets its own venv, no exceptions — five extra seconds now saves an afternoon of debugging a dependency conflict that has nothing to do with your actual code." This is not startup-specific caution — it is the near-universal default across professional Python teams, exactly because the failure mode above is so common and so time-consuming to diagnose after the fact.
Four Misconceptions About Modules and Environments
5 Interview Questions — With Complete Answers
Module & Environment Mistakes Beginners Make Constantly
Errors You Will Hit With Modules & Environments — And Exactly Why
🎯 Key Takeaways
- ✓A module is a single .py file; a package is a folder of modules (traditionally marked by __init__.py). Both are imported with dotted syntax.
- ✓Python resolves imports through sys.path, in order — the running script's own directory first, which is why naming a file the same as a standard-library module silently shadows it.
- ✓Prefer absolute imports over relative ones for clarity; relative imports also break when the file is run directly instead of as part of a package.
- ✓if __name__ == "__main__": lets a file be both an importable module and a standalone runnable script, without demo code firing on every import.
- ✓pip installs into whichever environment is currently active — always activate a project's venv before installing anything.
- ✓A virtual environment isolates a project's dependencies from every other project and the global environment, preventing version conflicts. Never commit .venv/ to version control.
- ✓requirements.txt records a project's dependencies so the same environment can be recreated elsewhere with pip install -r requirements.txt.
What comes next
Module 19 begins the Object-Oriented Python phase — classes, objects, and the __init__ method, from first principles.
Module 19 → Classes and Objects — The BasicsDiscussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.