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

Modules & Imports

1 · The lesson

read

Any .py file is a module. When your program gets long, or when you have code you want to reuse, you split it into modules and import them. The standard library, which is already installed with Python, gives you a few hundred more modules ready to use.

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, with no hyphens (my-module can't be imported) and 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 "geometry." prefix needed
print(PI)

# Form C: import with an alias (a short name)
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 very often and the prefix adds nothing useful.
import x as yLong names (import numpy as np) or local naming conflicts.

Star imports (from x import *) are discouraged. They fill your namespace with names you didn't choose, they hide where each name came from, and linters warn about them. The only place they are acceptable is in an interactive REPL session.


3. The Standard Library: Modules You Will Use Often

A few imports you will use all the time:

python
import os                               # files, environment variables, 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. Look through the docs index once. You don't need to memorise it. You only need a rough idea of what's there, so that you know what to search for.


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, because the guard skipped the block

Use it for:

  • Quick demos and smoke tests at the bottom of a module.
  • Command-line entry points: the call to the script's "main" function goes inside 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 also run all that demo code. 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 choose which names the package offers to other code (its 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 which submodule defines it.


6. Where Python Looks for Modules

When you write import geometry, Python searches in order:

1. Built-in modules that are compiled into Python (such as sys).
2. The directory of the script you're running.
3. Directories listed in the PYTHONPATH environment variable.
4. The standard library folders, where modules such as json and random live.
5. Site-packages: the folder 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, Python didn't find the name in any folder on sys.path. Usually the cause is 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. Each project gets its own. So your web scraper can use requests==2.31 while your client's old tool uses requests==2.25, and the two versions don't conflict in one 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 packages. 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. You don't commit a venv to git. You recreate it from requirements.txt.

There is no good reason to skip this step. If you install packages globally, you get code that "works on my machine" but not on anyone else's. See pip & the Standard Library for more.


8. Relative vs Absolute Imports

Inside a package, you can import siblings two ways:

python
# Absolute: the full path from the project root. Preferred.
from myapp.geometry.circles import circle_area

# Relative: "." means "this package", ".." means "the parent package".
from .circles import circle_area
from ..storage import json_store

Absolute imports work the same wherever the importing file is, they are easy to search for, and they don't break when you move the importing file. Use them by default. Relative imports have one good use: modules in the same package that depend closely on each other, where the relative path makes that connection clear.


Common Mistakes

1. Circular imports

a.py imports b.py, and b.py imports a.py. Python starts running a, reaches the import of b, starts running b, and then reaches the import of a. But a is only half loaded at that point. You get either an ImportError or a confusing AttributeError ("module has no attribute X", because X was not defined yet when b imported the half-loaded a).

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           # importing here, inside the function, breaks the cycle
    return helper(thing) + 1

Prefer the first fix. A circular dependency usually means your modules do too much, and splitting them makes the design clearer.

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 is in the script directory, step 2 of the search) and imported it instead of the standard library module. Rename your file to dice.py, roller.py or any name that isn't a standard library module name. The same problem happens with email.py, string.py, os.py and 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 replace your variable?

You can't tell which names are defined in your file and which came from the import. Linters warn about it, it makes the code harder to read, and it makes debugging harder. 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, it puts that file's folder at the start of sys.path and does not know that the file belongs to a package. So relative imports inside the file break. Run it as a module instead:

bash
python -m myapp.geometry.circles        # works, and Python knows the package

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

This installs pandas into your system Python. Six months later, a different project needs an older pandas. You change the version for one project, and the other project breaks. venv exists to prevent this problem. Always activate the venv first, then install.


🎯 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, then 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 removes empty pieces, which is exactly what you need for counting words. 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
"""A tiny word-count tool. Use it as a module or from the command line."""

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}

One file has two roles: a library and a command-line tool. The __name__ == "__main__" guard makes this possible.


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 fills the namespace with names and hides where they came from.
  • The standard library is very large 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: Sets, for fast checks on unique items.

Found this useful? Share it with a friend who’s learning Python.