Python · SQL · Web Dev · Java · AI/ML tracks launching soon — your one platform for all of IT
Intermediate+150 XP

Modules, Packages & Virtual Environments

import, pip, requirements.txt, and virtual environments — how real Python projects are actually structured.

35 min August 2026
// Part 01 — What a Module Actually Is

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 — a module you write yourself
# helpers.py
def greet(name):
    return f"Hello, {name}!"

PI_ISH = 3.14
main.py — importing your own module
# main.py, in the same folder as helpers.py
import helpers

print(helpers.greet("Asha"))   # "Hello, Asha!"
print(helpers.PI_ISH)          # 3.14

Three 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.

⚠️ Important
Never write 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.

// Part 02 — Packages

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.

A small package layout
myapp/
    __init__.py
    database.py
    utils/
        __init__.py
        formatting.py
        validation.py
Importing from a nested package
from 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.

// Part 03 — How Python Actually Finds an Import

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.

Inspecting the search path
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 packages

This 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.

⚠️ Important
Never name a file the same as a standard-library or installed package. If you see 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.
// Part 04 — Relative vs Absolute Imports

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").

Absolute import — the recommended default
# myapp/utils/validation.py
from myapp.utils.formatting import format_currency
Relative import — the same thing, written relatively
# 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

A common error — running a file with relative imports directly
# 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.validation
// Part 05 — __name__ == '__main__'

The 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.

Why this guard matters
# 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 fires

Without 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.

// Part 06 — pip and PyPI

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.

Core pip commands
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 package

requirements.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.

requirements.txt
requests==2.31.0
pandas==2.1.4
python-dateutil==2.8.2
Generating and installing from it
pip freeze > requirements.txt       # capture everything currently installed
pip install -r requirements.txt     # install everything a requirements.txt lists
🎯 Pro Tip
pip 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.
// Part 07 — Virtual Environments

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.

Creating and using a venv
# 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
deactivate
⚠️ Important
A very common beginner mistake: running pip 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.

// Part 08 — Real World
💼 What This Looks Like at Work

Onboarding at a Seattle Fintech Startup

Scenario — Fintech startup, Seattle · New engineer, day one

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.

The fix a senior teammate walks them through
# 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 machine

Why 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.

// Part 09 — Misconceptions

Four Misconceptions About Modules and Environments

✕ ""pip install installs a package for a specific project""
By default pip installs into whatever Python environment is currently active — which is the GLOBAL environment unless you have activated a venv first. pip has no inherent concept of "this project" at all; isolation is entirely something YOU set up with a venv.
✕ ""__init__.py has to contain code for a folder to be a package""
It can be completely empty — its presence alone (on older Python) or even its absence (Python 3.3+ namespace packages) is enough to make a folder importable. Real projects usually put re-export code in it anyway, but it is not required.
✕ ""Relative imports (from .module import x) are always safer than absolute ones""
Relative imports only work when the file is run as part of a package (e.g. with python -m), and break with an ImportError if the same file is ever run directly. Most style guides, including PEP 8, actually recommend absolute imports as the clearer default.
✕ ""Deleting a venv folder can lose my project's code""
A venv contains ONLY installed dependencies and a Python interpreter copy — never your own project code. Deleting .venv and recreating it with pip install -r requirements.txt is always safe and is a completely standard troubleshooting step.
// Part 10 — Interview Prep

5 Interview Questions — With Complete Answers

What is the difference between a module and a package?
A module is a single .py file. A package is a folder containing modules (and possibly sub-packages), traditionally marked with an __init__.py file. Both are imported with the same dotted syntax — a package import just has more dots when reaching into nested folders.
Why does a virtual environment matter — what actual problem does it solve?
Without one, pip install writes into a single shared global Python environment, so two projects needing different, incompatible versions of the same dependency cannot both be satisfied at once. A venv gives each project its own isolated set of installed packages, so version conflicts between unrelated projects become impossible by construction.
What does if __name__ == "__main__": actually check, and why is it useful?
Python sets a file's __name__ to "__main__" only when that file is executed directly, and to the module's real name when it is imported elsewhere. The guard lets a file define reusable functions/classes AND optionally run demo/CLI code — but only when run directly, never as a side effect of being imported by something else.
What is the actual search order Python uses to resolve an import?
sys.path, in order: the directory of the currently running script, PYTHONPATH entries if set, the standard library's own directories, then site-packages where pip-installed packages live. The first matching name anywhere in that ordered list wins — which is exactly why naming your own file "random.py" can silently shadow the real standard-library random module.
What is the purpose of requirements.txt, and how is it normally generated?
It records a project's dependencies (usually pinned to exact versions) in one file, so the exact same environment can be recreated elsewhere with pip install -r requirements.txt. It is commonly generated with pip freeze > requirements.txt after installing everything a project needs inside an active venv.
// Common Mistakes

Module & Environment Mistakes Beginners Make Constantly

Installing packages without activating the venv first
The install "succeeds" silently into the wrong environment (usually global), and the project then fails with ModuleNotFoundError despite the package appearing to be installed. Always check for the (.venv) prompt prefix before running pip install.
Naming a personal file the same as a standard-library or third-party module
A file called requests.py, json.py, or random.py in your project directory shadows the real module of the same name, because the running script's own directory is searched first. Rename the file.
Committing the .venv folder to version control
It is large, entirely machine-specific, and unnecessary — everyone who clones the repo should create their own venv from requirements.txt. Add .venv/ to .gitignore.
Using a relative import in a file meant to also be run directly
from .helpers import x raises "ImportError: attempted relative import with no known parent package" the moment the file is executed directly rather than imported as part of a package. Use an absolute import instead if the file needs to support both.
Forgetting to update requirements.txt after installing a new package
pip install works fine locally but the new dependency is invisible to anyone else who installs from requirements.txt — including CI and production. Run pip freeze > requirements.txt (or manually add the line) right after installing anything new.
// Error Library

Errors You Will Hit With Modules & Environments — And Exactly Why

ModuleNotFoundError: No module named 'requests'
Cause: The package genuinely is not installed in the currently active Python environment — either it was never installed, or it was installed into a different environment (e.g. globally, while a venv was active for the actual run).
Fix: Confirm the correct venv is active, then run pip install requests. If it still fails, run "pip show requests" and "which python" to confirm you are looking at the environment you think you are.
ImportError: attempted relative import with no known parent package
Cause: A file using a relative import (from .module import x) was run directly rather than as part of a package.
Fix: Either run it with python -m package.module instead of python package/module.py, or switch the file to an absolute import.
ModuleNotFoundError: No module named 'myapp'
Cause: Python could not find your own package on sys.path — usually because you ran a script from a different working directory than expected, so "the current directory" no longer contains myapp/.
Fix: Run the script from the project's root directory, or install the project itself in editable mode with "pip install -e ." so it is importable from anywhere.
AttributeError: module 'random' has no attribute 'randint'
Cause: A local file in the project happens to be named random.py, shadowing the real standard-library random module for every import in the program.
Fix: Rename the local file to something that does not collide with a standard-library or installed package name.

🎯 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 Basics
Share

Discussion

0

Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.

Continue with GitHub
Loading...