PythonMastery
beginner 16 min read · lesson 15 of 19 in Python Fundamentals

Modules & Imports

1 · The lesson

read

Any .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:

python
# 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:

python
# 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

python
# 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)
FormWhen to use it
import xDefault. The prefix x.thing makes the source of thing obvious.
from x import thingWhen you use thing constantly and the prefix is noise.
import x as yLong 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:

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

python
# 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:

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

python
myapp/
    __init__.py
    main.py
    geometry/
        __init__.py
        circles.py
        triangles.py
    storage/
        __init__.py
        json_store.py

You import from a package the same way as a module, with dots for the path:

python
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:

python
# 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:

python
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:

bash
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:

python
# 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.py that both a and b import. The cycle disappears.
  • Move the import inside the function that uses it. By the time the function runs, both modules are fully loaded.
python
# 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

python
# 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

python
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

bash
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:

bash
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

bash
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:

python
# 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
python
# 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:

bash
python wordcount.py myessay.txt
#  chars: 4218
#  words: 712
#  lines: 38
python
# 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 .py file is a module. Import it with import x, from x import thing, or import 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, re are 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__.py to 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.