Modules & Imports
1 · The lesson
readAny .py file is a module. Once you have more than ~200 lines of code, or any code you want to reuse, you split it into modules and import them. The standard library — already on your machine — is a few hundred more modules waiting to do work for you.
This lesson covers the three import forms, the if __name__ == "__main__": idiom, packages, where Python looks for modules, and why every project should live inside a virtual environment.
1. A Module Is Just a File
Create geometry.py:
# geometry.py import math def circle_area(radius): return math.pi * radius ** 2 def circle_circumference(radius): return 2 * math.pi * radius PI = math.pi
That file is now a module named geometry. From another file in the same directory:
# main.py import geometry print(geometry.circle_area(5)) # 78.539... print(geometry.PI) # 3.141592653589793
The module's name is its filename without .py. Names should be lowercase_with_underscores — no hyphens (my-module can't be imported), no spaces.
2. Three Ways to Import
# Form A — import the whole module import geometry geometry.circle_area(5) # Form B — import specific names from geometry import circle_area, PI circle_area(5) # no prefix print(PI) # Form C — import with an alias import geometry as geo geo.circle_area(5) from geometry import circle_area as area area(5)
| Form | When to use it |
|---|---|
import x | Default. The prefix x.thing makes the source of thing obvious. |
from x import thing | When you use thing constantly and the prefix is noise. |
import x as y | Long names (import numpy as np) or local naming conflicts. |
Star imports — from x import * — are discouraged. They flood your namespace with names you didn't choose, hide where each name came from, and trip every linter. The only place you'll legitimately see them is at an interactive REPL.
3. The Standard Library — Modules That Pay Rent
A few imports you'll reach for constantly:
import os # filesystem, env vars, paths print(os.getcwd()) # current working directory import json # parse and produce JSON data = json.loads('{"name": "Linus"}') import datetime print(datetime.date.today()) # 2026-05-14 import random print(random.choice(["a", "b", "c"])) # one of the three
Others worth knowing exist: re (regex), pathlib (modern paths), collections (specialised containers), itertools, functools, csv, urllib, subprocess. Browse the docs index once — you don't need to memorise it, you need to know roughly what's there so you can search.
4. The if __name__ == "__main__": Idiom
Every module has a built-in variable __name__. When Python runs the file directly, __name__ is "__main__". When the file is imported by another file, __name__ is the module's name ("geometry", etc.).
# geometry.py def circle_area(radius): return 3.14159 * radius ** 2 if __name__ == "__main__": # This block only runs when you do: python geometry.py # It does NOT run when another file does: import geometry print("Demo:") print(circle_area(5))
Run it both ways:
python geometry.py # prints "Demo:" and "78.53975" python -c "import geometry" # prints nothing — the guard skipped the block
Use it for:
- Quick demos and smoke tests at the bottom of a module.
- Command-line entry points — the script's "main" function lives behind the guard.
- Code that should only execute when the file is the program, not when it's a library.
Without the guard, importing your file would run all that demo code as a side effect of the import. The guard makes a file usable as both a script and a library.
5. Packages
A package is a directory of modules that contains a special file, __init__.py (it can be empty). The directory name becomes the import name.
myapp/
__init__.py
main.py
geometry/
__init__.py
circles.py
triangles.py
storage/
__init__.py
json_store.pyYou import from a package the same way as a module, with dots for the path:
from myapp.geometry.circles import circle_area from myapp.storage import json_store import myapp.geometry.triangles as tri
The __init__.py file runs the first time anything in that package is imported. Use it to expose a curated public API:
# myapp/geometry/__init__.py from .circles import circle_area, circle_circumference from .triangles import triangle_area
Now callers can write from myapp.geometry import circle_area without knowing or caring which submodule it lives in.
6. Where Python Looks for Modules
When you write import geometry, Python searches in order:
1. Built-in modules (sys, math, etc. — compiled in).
2. The directory of the script you're running.
3. Directories listed in the PYTHONPATH environment variable.
4. Site-packages — where pip install puts third-party modules.
The full list lives in sys.path:
import sys for p in sys.path: print(p)
If an import fails with ModuleNotFoundError, one of those four places didn't contain what you wrote. Usually it's a typo or a missing pip install.
7. Virtual Environments — Use One Per Project
A virtual environment (venv) is a self-contained Python installation that lives inside your project folder. Every project gets its own — so your web scraper using requests==2.31 and your client's legacy tool using requests==2.25 don't fight over the same site-packages directory.
Three commands per project:
python -m venv .venv # 1. create the venv in a folder called .venv # 2. activate it # macOS / Linux: source .venv/bin/activate # Windows PowerShell: .venv\Scripts\Activate.ps1 pip install requests pytest # 3. install deps — they go into .venv, not system Python
While activated, your shell's python and pip point at the venv. Deactivate with deactivate. Add .venv/ to .gitignore — venvs are recreated from requirements.txt, not committed.
There's no good reason to skip this step. Installing globally is how you end up with "it works on my machine" — see pip & the Standard Library for more.
8. Relative vs Absolute Imports
Inside a package, you can import siblings two ways:
# Absolute — full path from the project root. Preferred. from myapp.geometry.circles import circle_area # Relative — dots mean "this package", ".." means "parent package". from .circles import circle_area from ..storage import json_store
Absolute imports work the same no matter where the file ends up; they're searchable; they don't break when you move things. Use them by default. Relative imports have one legitimate use: tightly coupled sibling modules within the same package where the relative path makes the relationship obvious.
Common Mistakes
1. Circular imports
a.py imports b.py, and b.py imports a.py. Python tries to run a, hits the import of b, starts running b, hits the import of a — which is half-loaded — and you get either an ImportError or a baffling AttributeError ("module has no attribute X" because X hadn't been defined yet when the partial import happened).
Two fixes:
- Extract the shared types or constants into a third module
c.pythat bothaandbimport. The cycle disappears. - Move the import inside the function that uses it. By the time the function runs, both modules are fully loaded.
# b.py def process(thing): from a import helper # local import breaks the top-level cycle return helper(thing) + 1
Prefer the first fix. A circular dependency is usually a sign your modules are doing too much; splitting them clarifies the design.
2. Naming your file random.py or math.py
# random.py (your own file) import random print(random.randint(1, 6)) # AttributeError: module 'random' has no attribute 'randint'
Python found your random.py first (it's in the script directory, step 2 of the search) and imported it instead of the stdlib. Rename your file to dice.py, roller.py, anything that isn't a stdlib module name. Same trap with email.py, string.py, os.py, json.py.
3. from x import * and not knowing what came in
from tkinter import * # Now your namespace contains 150+ names. Did one of them shadow your variable?
You can't tell what's defined locally and what came from the import. Linters flag it; readers hate it; debugging is painful. Always import the specific names you need, or use a module prefix.
4. Running a submodule directly
python myapp/geometry/circles.py # often fails with ImportError
When Python runs a file directly, that file becomes the top of sys.path and its package context is lost — so relative imports inside it break. Run it as a module instead:
python -m myapp.geometry.circles # works; package context preserved
The -m flag tells Python "import this thing, then run it". Use it whenever a submodule inside a package is meant to be executable.
5. Installing globally instead of in a venv
pip install pandas # without an activated venv
You've just dumped pandas into your system Python. Six months later, a different project needs an older pandas. You upgrade. The first project breaks. You downgrade. The second breaks. This loop is the entire reason venv exists. Activate first, then install — always.
🎯 Your Turn — A Reusable Word-Count Module
Build wordcount.py. It should:
1. Define a function count(text) that returns a dict with keys chars, words, and lines.
2. Include an if __name__ == "__main__": block that:
- Reads a filename from sys.argv[1].
- Opens it, reads the contents, calls count, and prints the result nicely.
The module must be importable (so the file-reading code can't run on import) and runnable as python wordcount.py somefile.txt.
Skeleton:
# wordcount.py import sys def count(text): # TODO 1: count characters — len(text) is fine # TODO 2: count words — split on whitespace, count the pieces # TODO 3: count lines — splitlines() is cleaner than splitting on "\n" return {"chars": ..., "words": ..., "lines": ...} if __name__ == "__main__": # TODO 4: get filename from sys.argv[1] # TODO 5: read the file's contents # TODO 6: call count() and print each key/value ...
Hint 1 — Splitting text
text.split() with no argument splits on any run of whitespace and discards empty pieces — perfect for word counting. text.splitlines() returns a list of lines without the trailing newline characters.
Hint 2 — Reading a file
with open(path, encoding="utf-8") as f: text = f.read() gets you the whole contents as a single string. sys.argv is a list; index 0 is the script name, 1 is the first argument.
Show full solution
# wordcount.py """Tiny word-count utility — usable as a module or a CLI tool.""" import sys def count(text: str) -> dict: """Return character, word, and line counts for the given text.""" return { "chars": len(text), "words": len(text.split()), "lines": len(text.splitlines()), } if __name__ == "__main__": if len(sys.argv) != 2: print("Usage: python wordcount.py <filename>") sys.exit(1) filename = sys.argv[1] with open(filename, encoding="utf-8") as f: text = f.read() result = count(text) for key, value in result.items(): print(f"{key:>6}: {value}")
Try both modes:
python wordcount.py myessay.txt # chars: 4218 # words: 712 # lines: 38
# Or import it from another file: from wordcount import count print(count("hello world\nsecond line")) # {'chars': 23, 'words': 4, 'lines': 2}
Same file, two roles — library and CLI tool. That's the __name__ == "__main__" pattern doing real work.
What You Learned
- Any
.pyfile is a module. Import it withimport x,from x import thing, orimport x as y. - Avoid
from x import *— it pollutes the namespace and hides origins. - The standard library is enormous and already installed.
os,json,datetime,random,pathlib,reare starting points. if __name__ == "__main__":lets a file be both an importable module and a runnable script.- Packages are directories with
__init__.py. Use the__init__.pyto expose a clean public API. - Python searches built-ins, the script's directory,
PYTHONPATH, and site-packages — in that order. - Every project gets a virtual environment:
python -m venv .venv, activate,pip install. - Prefer absolute imports. Don't name files after stdlib modules. Run submodules with
python -m pkg.sub.
Next: pip & the Standard Library — installing third-party packages and the stdlib gems worth knowing about.