Python Modules — Complete Guide
A complete, professional guide to Python modules — every import variation, the module search path, module caching, creating custom modules, packages, and how to organize real Python projects.
Last Updated
March 2026
Read Time
26 min
Level
Intermediate
What is a Module in Python?
A module in Python is simply a file containing Python code — functions, classes, variables, or runnable statements — that can be imported and reused in other Python files. Every single .py file you write is automatically a module the moment another file imports it; there is no special syntax or declaration required to "turn a file into a module" — the file itself already is one.
Modules exist to solve one of the oldest problems in software: organizing code so it can be reused without being copy-pasted, and so a large program can be split into smaller, independently understandable, independently testable pieces. Instead of writing every function your program needs in one enormous file, you group related functionality into separate modules — a database.py module for data access, a validators.py module for input checks, a utils.py module for shared helpers — and import exactly what each part of your program needs from each one.
Python's own standard library is itself built almost entirely out of modules — os, sys, json, random, datetime, and hundreds of others are simply Python (and some C) files, organized and shipped with every Python installation, that you import exactly the same way you would import your own code. Understanding modules is therefore not a niche skill — it is the mechanism behind literally every import statement you have ever written.
Three Categories of Python Modules
Every module you import falls into one of three categories, each with a different origin and a different level of trust and stability.
A special subset of built-in modules — like sys and _thread — are actually compiled directly into the CPython interpreter itself, written in C rather than Python, and are technically distinct from standard library modules that happen to be .py files shipped alongside the interpreter (like json or random). From an import statement's point of view, however, both behave identically — you import them exactly the same way, and the distinction rarely matters in day-to-day code.
Every Form of the import Statement
Python offers several distinct ways to bring a module's contents into your current file's namespace, each with a different trade-off between convenience and namespace clarity.
import math
print(math.pi) # 3.141592653589793
import numpy as np
print(np.array([1, 2, 3]))
from math import sqrt
print(sqrt(16)) # 4.0
from math import sqrt as square_root
print(square_root(25)) # 5.0
from math import * # imports everything — avoid in real code
print(pi, sqrt(9))The wildcard form, from module import *, is almost universally discouraged in production code because it silently pulls every public name from that module directly into your current namespace, with no prefix at all — making it impossible to tell, just by reading a line of code, which module a given name actually came from, and creating a real risk of one import silently overwriting a name defined elsewhere in your file.
How import Actually Works — The Module Search Path
When Python encounters an import module_name statement, it doesn't search your entire hard drive — it checks a very specific, ordered list of locations called the module search path, stored as a list of strings in sys.path. Python checks each location in order and imports the first matching module it finds; if none match, it raises ModuleNotFoundError.
- ▶
1. The directory of the running script — Python always checks the folder containing the file you actually executed first, so local modules right next to your main script are found immediately.
- ▶
2. PYTHONPATH environment variable — Any directories listed in this optional environment variable are checked next, letting you add custom search locations without modifying code.
- ▶
3. Standard library directories — The installation-specific folders containing os, sys, json, and every other built-in module that ships with Python.
- ▶
4. site-packages directory — Where pip installs third-party packages (numpy, requests, django, etc.), typically inside your virtual environment if one is active.
import sys
for path in sys.path:
print(path)
# You can even modify it at runtime (not usually recommended):
sys.path.append("/my/custom/module/directory")The import Process — Flowchart
The diagram below shows exactly what happens the moment Python executes an import statement — from checking the module cache through searching sys.path to actually executing the module's code.
Code Execution Flow — from source to output
Key insight: a module's top-level code only ever runs once per program execution, no matter how many different files import it. The very first import triggers full execution and caches the result in sys.modules; every subsequent import anywhere else in the program simply reuses that cached module object instantly.
Creating and Importing Your Own Module
Writing your own module requires no special syntax at all — you simply save Python code in a .py file, and any other file in the same directory (or on the module search path) can import it by that filename, without the .py extension.
# shapes.py — a small custom module
PI = 3.14159
def circle_area(radius):
return PI * radius ** 2
def rectangle_area(length, width):
return length * width
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return circle_area(self.radius)import shapes
print(shapes.PI) # 3.14159
print(shapes.circle_area(5)) # 78.53975
c = shapes.Circle(10)
print(c.area()) # 314.159
# Or, importing specific names directly:
from shapes import circle_area, Circle
print(circle_area(3))Everything defined at the top level of shapes.py — variables, functions, and classes — becomes an attribute accessible through shapes.whatever_name once imported. This is exactly how the standard library works too: math.pi and math.sqrt are simply attributes of the math module object, defined at that module's top level.
The if __name__ == '__main__' Pattern
Every Python module has a special built-in variable called __name__. When a file is run directly (e.g. python shapes.py), Python sets that file's __name__ to the string "__main__". When that same file is instead imported by another module, its __name__ is set to its actual module name (e.g. "shapes") instead.
PI = 3.14159
def circle_area(radius):
return PI * radius ** 2
def _run_self_tests():
assert circle_area(1) == PI
assert circle_area(0) == 0
print("All shape tests passed.")
if __name__ == "__main__":
# This block ONLY runs when shapes.py is executed directly,
# never when it's imported by another file.
_run_self_tests()This pattern is extremely common because it lets a single file serve two roles at once: a reusable module and a standalone, runnable script. Library code, quick self-tests, command-line entry points, and demonstration code are all placed inside the if __name__ == "__main__": block specifically so they never execute accidentally as a side effect of someone simply importing the file elsewhere.
Modules vs Packages — What's the Difference?
A module is a single .py file. A package is a directory containing multiple modules, organized together, with an optional __init__.py file that marks the directory as importable as a single unit. Packages let you group many related modules under one hierarchical namespace, exactly the way the standard library groups things like os.path or third-party libraries group things like numpy.linalg.
myapp/
├── __init__.py
├── models.py
├── views.py
└── utils/
├── __init__.py
├── validators.py
└── formatters.pyWith this structure, other code can write from myapp.utils import validators or from myapp.utils.formatters import format_date — the dots in the import path directly mirror the folder structure on disk. The __init__.py file (even when left completely empty) signals that a directory should be treated as a package, and its code — if any — runs automatically the first time any module inside that package is imported.
Built-in Module Attributes
Every imported module automatically carries several special dunder (double-underscore) attributes that describe the module itself — useful for introspection, debugging, and dynamic tooling.
import json
print(json.__name__) # 'json'
print(json.__file__) # the actual path to json's source file
print(json.__doc__) # the module's docstring, if it has one
print(dir(json)) # a list of every name defined in the module- ▶
__name__— The module's own name as a string; equals "__main__" only when the file is run directly, as covered above. - ▶
__file__— The absolute file path the module was loaded from; not present for built-in C modules like sys. - ▶
__doc__— The module's docstring — whatever triple-quoted string, if any, appears at the very top of the file. - ▶
__package__— The name of the package this module belongs to, or an empty string for top-level modules. - ▶
dir(module)— Not an attribute itself, but a built-in function that lists every name (functions, classes, variables) defined in the module.
Module Caching and Reloading with importlib
As covered in the import flowchart, Python caches every imported module in the sys.modules dictionary, keyed by module name. This is why importing the same module from ten different files in the same program only executes that module's top-level code once — every subsequent import is effectively a dictionary lookup, not a re-execution.
import sys
import json
print('json' in sys.modules) # True — already cached after the import above
print(sys.modules['json'] is json) # True — same objectThis caching becomes relevant during interactive development — if you edit a module's source file while it's already loaded in a running interactive session (like a Jupyter notebook or the Python REPL), a plain import module_name won't pick up your changes, because the cached version is reused. The importlib.reload() function exists specifically to force Python to re-execute a module's code and refresh the cached version.
import importlib
import shapes
# ... you edit shapes.py in your editor while the session is still running ...
importlib.reload(shapes) # forces shapes.py to re-execute and refreshimportlib.reload() is a development-time convenience tool and is rarely, if ever, used in production code — in a normal program run from start to finish, every module is naturally imported (and cached) exactly once, which is the correct and expected behaviour.
How a Python Project's Modules Fit Together
In a well-organized Python project, modules typically form a layered dependency structure — lower layers contain no dependencies on higher ones, keeping the overall system easy to reason about and test.
A common design goal is to avoid circular imports — where module A imports from module B, and module B also (directly or indirectly) imports from module A — which Python can only partially tolerate and which usually signals that shared logic between the two modules should be extracted into a third, lower-level module that both can depend on instead.
Practice: A Small Multi-Module Project
This example shows a realistic small project split across two modules, demonstrating imports, module-level constants, and the __main__ guard together.
"""Utility functions for temperature conversions."""
ABSOLUTE_ZERO_C = -273.15
def celsius_to_fahrenheit(c):
if c < ABSOLUTE_ZERO_C:
raise ValueError(f"{c}°C is below absolute zero")
return (c * 9 / 5) + 32
def fahrenheit_to_celsius(f):
return (f - 32) * 5 / 9
if __name__ == "__main__":
print(celsius_to_fahrenheit(100)) # 212.0
print(fahrenheit_to_celsius(32)) # 0.0from temperature import celsius_to_fahrenheit
def print_report(city, celsius):
fahrenheit = celsius_to_fahrenheit(celsius)
print(f"{city}: {celsius}°C ({fahrenheit}°F)")
if __name__ == "__main__":
print_report("Delhi", 38)
print_report("London", 12)Output
Delhi: 38°C (100.4°F) London: 12°C (53.6°F)Practice This Code — Live Editor
Python Module Best Practices
These practices help keep modules organized, predictable, and easy for other developers (including future you) to navigate.
- ▶
Avoid wildcard imports (from module import *) — They make it impossible to tell where a given name came from just by reading the code, and risk silently overwriting existing names.
- ▶
One clear responsibility per module — A module named validators.py should contain validation logic, not database queries or HTTP handling; mixed responsibilities make modules harder to test and reuse.
- ▶
Always guard runnable code with if __name__ == '__main__': — Prevents demonstration code, self-tests, or CLI entry points from executing accidentally when the file is imported elsewhere.
- ▶
Group standard library, third-party, and local imports separately — PEP 8 recommends three blocks, each internally alphabetized: standard library first, then third-party packages, then your own local modules.
- ▶
Give modules a top-level docstring — A short triple-quoted string at the very top explaining the module's purpose shows up automatically via __doc__ and help().
- ▶
Avoid circular imports by extracting shared code — If two modules need to import from each other, move the shared logic into a third, lower-level module both can depend on instead.
- ▶
Use packages (with __init__.py) once a project grows past a handful of files — A flat pile of unrelated .py files becomes hard to navigate; grouping related modules into packages keeps structure legible as a project scales.
- ▶
Never rely on module-level mutable state across imports without care — Since a module's code runs only once and is cached, any mutable global variable it defines is genuinely shared and mutated by every part of the program that imports it.
Advantages and Disadvantages of Python's Module System
Python's module system is simple and powerful, but understanding its trade-offs helps you structure larger projects correctly from the start.
Python Modules — Interview Questions
Frequently asked interview questions about Python modules, packages, and the import system.
Practice Questions — Test Your Understanding
Work through these practice questions on Python modules and the import system. Try to reason through each before checking the answer.
1. If module_a.py is imported by three different files in the same running program, how many times does its top-level code actually execute?
Easy2. What is printed if utils.py contains print('loading utils') at its top level, and it is imported by two different files run in the same program?
Medium3. What is the value of __name__ inside greet.py when it is run directly with 'python greet.py', versus when it is imported by another file as 'import greet'?
Easy4. Why might 'from . import helpers' fail with an ImportError when a file inside a package is executed directly as a script, but work fine when that same package is imported normally?
Hard5. Two modules, models.py and services.py, each try to import from the other at the top of their files. What problem does this create?
Hard6. What is the output order if config.py defines DEBUG = True at the top level, and two different files both do 'from config import DEBUG' and then modify their own local DEBUG variable?
Hard7. Does sys.path include the directory of the script you're currently running?
Medium8. What is the practical difference between 'import json' and 'from json import loads'?
MediumConclusion — Structuring Python Code with Modules
Modules are the foundation Python's entire code organization system is built on — from the standard library, to every third-party package on PyPI, to your own project's files. Understanding how import actually works, from the module search path through to caching in sys.modules, demystifies behaviour that otherwise seems almost magical: why a module's print statements only ever run once, why editing a file mid-session doesn't automatically update an already-imported module, and why circular imports fail in ways that can be confusing without this mental model.
The most valuable habits to carry forward: keep each module focused on one clear responsibility, guard runnable code with if __name__ == "__main__": so it never executes as a side effect of importing, avoid wildcard imports so every name's origin stays traceable, and reach for packages once a project grows beyond a handful of loosely related files, so its structure stays legible as it scales.
From here, natural next topics include Python packages in greater depth (namespace packages, relative imports, and the __all__ variable), virtual environments for isolating third-party dependencies per project, and packaging your own project for distribution via PyPI using tools like setuptools or poetry.