🐍 Python

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.

CategoryExamplesWhere It LivesInstalled How
Built-in / Standard Libraryos, sys, math, json, datetime, reBundled with the Python interpreter itselfComes free with every Python installation
Third-Party Packagesnumpy, requests, django, flask, pandasDownloaded from PyPI (Python Package Index)pip install <package-name>
User-Defined Modulesutils.py, models.py, config.py (your own files)Anywhere in your project's file structureWritten directly by you — no installation needed

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.

SyntaxExampleWhat Gets ImportedAccess Pattern
Basic importimport mathThe whole module, as an objectmath.sqrt(16)
Import with aliasimport numpy as npThe whole module, under a shorter namenp.array([1,2,3])
From-import a namefrom math import sqrtJust that one function/class/variablesqrt(16) — no prefix needed
From-import multiple namesfrom math import sqrt, piSeveral specific names at oncesqrt(16), pi directly
From-import with aliasfrom math import sqrt as square_rootOne name, renamed locallysquare_root(16)
Wildcard importfrom math import *Every public name in the modulesqrt(16), pi, all others directly — discouraged
Relative import (inside packages)from . import helpersA sibling module within the same packagehelpers.some_function()
🐍 Pythonimport_variations.py
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.

🐍 Pythoninspect_sys_path.py
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.

▶️ import module_nameStatement executes
checks
❓ Already in sys.modules?Check the module cache first
yes, cached
⚡ Use cached moduleInstant — no re-execution
reused
🔎 Search sys.pathCheck each directory in order
searches
❓ Module file found?Matching .py file or package?
not found
🚫 ModuleNotFoundErrorNo match anywhere in sys.path
⚙️ Compile to bytecodeCached as .pyc in __pycache__
compiled
▶️ Execute module top to bottomRuns once, defines names
finishes
💾 Store in sys.modulesCached for any future import
cached
🏁 Bind name in current scopeModule now usable

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.

🐍 Pythonshapes.py
# 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)
🐍 Pythonmain.py
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.

🐍 Pythonshapes_with_main.py
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.

AspectModulePackage
What it isA single .py fileA directory containing modules (and possibly sub-packages)
Exampleshapes.pymyapp/ containing models.py, views.py, utils.py
Marker file neededNone — any .py file is a module__init__.py (optional since Python 3.3, but still common)
Import syntaximport shapesimport myapp or import myapp.models
NamespacingFlat — one levelHierarchical — supports nested sub-packages
Real-world examplejson.py inside the standard libraryThe numpy package, containing numpy.linalg, numpy.random, etc.
🐍 Pythonpackage_structure.txt
myapp/
├── __init__.py
├── models.py
├── views.py
└── utils/
    ├── __init__.py
    ├── validators.py
    └── formatters.py

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

🐍 Pythonmodule_attributes.py
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.

🐍 Pythonsys_modules_cache.py
import sys
import json

print('json' in sys.modules)   # True — already cached after the import above
print(sys.modules['json'] is json)   # True — same object

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

🐍 Pythonimportlib_reload.py
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 refresh

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

Entry Point
main.pyif __name__ == '__main__': blockImports from every layer below
Application Modules / Packages
myapp.viewsmyapp.servicesmyapp.models
Shared Utility Modules
utils.validatorsutils.formattersconfig.py
Third-Party Packages (pip-installed)
requests, django, numpyInstalled into site-packages
Python Standard Library
os, sys, json, datetime, reBundled with the interpreter

Architecture Diagram

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.

🐍 Pythontemperature.py
"""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.0
🐍 Pythonweather_report.py
from 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.

✅ Advantages
Zero Boilerplate to Create a ModuleAny .py file is automatically a valid, importable module — no declarations, headers, or special syntax required.
Automatic Caching via sys.modulesA module's top-level code runs only once no matter how many times it's imported, keeping large programs efficient.
Clean, Hierarchical NamespacingPackages let related modules be grouped and accessed through readable dotted paths like myapp.utils.validators.
Massive, Ready-Made Standard LibraryHundreds of built-in modules cover file I/O, networking, data formats, and more, without installing anything extra.
Encourages Reusable, Testable CodeSplitting logic into focused modules makes each piece easier to unit test and reuse across different projects.
The __main__ Guard Enables Dual-Purpose FilesA single file can act as both an importable library and a standalone runnable script, without conflict.
❌ Disadvantages
Circular Imports Can Cause Real ProblemsTwo modules importing from each other can raise ImportError or produce partially-initialized modules, depending on import order.
Wildcard Imports Hurt Readabilityfrom module import * obscures where names actually come from and risks silent name collisions.
Module-Level Mutable State Is Shared EverywhereA mutable global variable defined in a module is the same object across every part of the program that imports it, which can cause subtle bugs if not managed carefully.
sys.path Search Can Be Confusing for BeginnersUnderstanding why an import succeeds or fails often requires knowing exactly what's in sys.path, which isn't always obvious.
Relative Imports Have Quirky RulesRelative imports (from . import x) only work properly inside a recognized package, which trips up beginners running a script directly instead of as part of a package.

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?

Easy

2. 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?

Medium

3. 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'?

Easy

4. 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?

Hard

5. 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?

Hard

6. 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?

Hard

7. Does sys.path include the directory of the script you're currently running?

Medium

8. What is the practical difference between 'import json' and 'from json import loads'?

Medium

Conclusion — 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.

SituationRecommended Approach
Need one specific function from a modulefrom module import function_name
Need many names, want clear origin trackingimport module, then module.name everywhere
Module name is long or awkwardimport long_module_name as short_alias
File should be both a library and a scriptWrap runnable code in if __name__ == '__main__':
Project has grown past a handful of filesOrganize into packages with __init__.py
Two modules seem to need each other❌ Circular import — extract shared code to a third module
Tempted to use from module import *❌ Avoid — import specific names instead

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.

Frequently Asked Questions (FAQ)