As programs grow, putting everything in one file becomes unmanageable. Python lets you split code across multiple files using modules. A module is simply a .py file that can be imported by other files.
What a module is
Any .py file is a module. The filename (without .py) becomes the module name:
project/
├── main.py
├── utils.py
└── config.py
# utils.py
def format_name(first, last):
return f"{first} {last}"
def is_valid_email(email):
return "@" in email
# main.py
import utils
name = utils.format_name("Ada", "Lovelace")
print(name) # Ada Lovelace
When Python sees import utils, it looks for a file called utils.py in the same directory (and in other locations on the module search path). It executes the file once and makes its contents available under the utils name.
Import styles
There are several ways to import:
import utils # access as utils.format_name()
from utils import format_name # access directly as format_name()
from utils import format_name as fn # access as fn()
from utils import * # imports everything — avoid this
How import works
When you import a module:
- Python checks if it has already been imported (in
sys.modules) - If not, it finds and executes the file top to bottom
- It creates a module object with all the module’s names
- It binds the module (or selected names) in your file’s namespace
A module is executed only once per program run. Subsequent imports reuse the cached module:
# config.py
print("Loading config...") # prints only once, no matter how many files import this
DEBUG = True
This is why import statements can appear anywhere in your code — at the top, inside functions, or conditionally. The convention is to place them at the top of the file so dependencies are clear.
Packages
A package is a directory with an __init__.py file. It groups related modules together:
project/
├── main.py
└── myapp/
├── __init__.py
├── utils.py
└── models.py
# main.py
from myapp.utils import format_name
from myapp import models
The __init__.py file can be empty. It tells Python that the directory is a package. It can also contain code that runs when the package is imported, or re-export names for convenience:
# myapp/__init__.py
from .utils import format_name
from .models import User
This lets users write from myapp import format_name instead of from myapp.utils import format_name.
Relative imports
Inside a package, you can use relative imports with dots:
# myapp/handlers.py
from .models import User # . means "from this package"
When a package is nested inside another package, you can go up levels:
# src/myproject/handlers.py — inside a larger package
from ..utils import helper # .. means "from the parent package"
Relative imports only work inside packages (directories with __init__.py). For simple scripts in a single directory, use absolute imports.
The module search path
When you import something, Python looks for it in sys.path, which contains:
- the directory containing the script being run (or the current directory for
python -m) - directories from the
PYTHONPATHenvironment variable - the standard library directory
- the site-packages directory where installed packages live
Built-in modules like sys and math are always available regardless of the search path.
You can inspect the search path:
import sys
print(sys.path)
This matters when you get ModuleNotFoundError. Python cannot find the file in any of these locations. The most common causes:
- the file is not in the same directory or on the path
- you are running a script from a different working directory
- there is a naming conflict with a standard library module
Writing a utility module
A utility module is a file of helper functions that other parts of your program import:
# fileutils.py
"""Utility functions for file operations."""
import os
import json
def read_json(path):
"""Read and return parsed JSON from a file."""
with open(path) as f:
return json.load(f)
def ensure_dir(path):
"""Create a directory if it does not exist."""
os.makedirs(path, exist_ok=True)
def list_files(directory, extension=".py"):
"""Return a list of files with the given extension in the directory."""
return [
f for f in os.listdir(directory)
if f.endswith(extension)
]
Other files import what they need:
# main.py
from fileutils import read_json, ensure_dir
config = read_json("config.json")
ensure_dir("output")
What is executable vs what is importable
Not every .py file should be imported. Some files are scripts — they are meant to be run directly:
# migrate.py — a script to run, not import
from fileutils import read_json
data = read_json("data.json")
process(data)
Other files are modules — they define functions and classes for other files to use:
# fileutils.py — a module to import
def read_json(path):
...
A file can serve as both if you guard the executable part:
# process.py
from fileutils import read_json
def main():
data = read_json("data.json")
print(data)
if __name__ == "__main__":
main()
__name__ is set to "__main__" when the file is run directly, but to the module name when it is imported. This pattern lets a file work as both a script and a module.
What to carry forward
- any
.pyfile is a module import moduleloads the file and gives access asmodule.namefrom module import nameimports a specific name directly- modules are executed once and cached
- packages are directories with
__init__.py - put imports at the top of your file
- write utility modules with focused, reusable functions
- use
if __name__ == "__main__"to guard script-only code
Modules are how real Python programs are organized. The next lesson tours the standard library — the modules Python ships with out of the box.