Python Modules

A module in Python is simply a file containing Python code — definitions, functions, classes, and runnable statements — that you can reuse in other programs by importing it. Modules are the basic unit of code organization in Python: instead of writing every program as one giant file, you split related functionality into separate files and pull in only what you need. Every script you’ve ever run with python3 script.py is itself a module, and the entire Python standard library (math, os, random, json, and hundreds more) is just a large collection of modules. Understanding how modules are found, loaded, and cached is essential to writing organized programs and to using third-party packages effectively.

Overview: How Python Modules Work

Every time you save Python code in a file ending in .py, you’ve created a module. The filename, minus the extension, becomes the module’s name — a file called geometry.py defines a module named geometry. Modules exist to solve a simple problem: as programs grow, keeping every function, class, and constant in one file becomes unmanageable. By splitting code across files and importing what you need, you get reusability (write a function once, use it everywhere), organization (related code lives together), and namespacing — two modules can each define a function called run() without colliding, because you always reach it through the module: geometry.run() versus physics.run().

When you write import geometry, Python does not copy-paste the file’s text into your program. It creates a module object — a real object of type module — executes the entire file’s code top to bottom exactly once, and stores every name defined at the top level of that file (functions, classes, variables) as an attribute on that object. The name geometry in your code is then just a variable referring to this module object. That is why you write geometry.area_circle(5) instead of area_circle(5): you are doing attribute access on an object, exactly like any other object.attribute lookup.

Python keeps a cache of every module it has already imported in a dictionary called sys.modules, keyed by module name. The first time a module is imported anywhere in a program, Python locates the file, executes it, and stores the resulting module object in this cache. Every later import of that same module — even from a different file — just looks it up in sys.modules and reuses the same object instead of re-running the file. This has two consequences worth internalizing: module-level code (print statements, connections opened at the top of a file) runs only once no matter how many times the module is imported, and every part of a program shares the exact same module object, so mutating a module-level variable in one place is visible everywhere else that imports it.

Packages extend this idea to directories: a folder containing an __init__.py file (or, since Python 3.3, even without one, as a "namespace package") is treated as a package, and its subfolders and files become submodules reachable with dotted paths like package.subpackage.module. Packages get their own lesson, but it helps to know now that a package is really just a module that can contain other modules.

Syntax

Python offers several forms of the import statement, each binding names into your current namespace slightly differently:

import module_name
import module_name as alias
from module_name import name
from module_name import name as alias
from module_name import name_one, name_two
from module_name import *
import package_name.module_name

This snippet is a syntax reference, not a program meant to be run as-is — replace module_name and package_name with real modules. The table below explains each form.

Form Meaning
import module Imports the module object; access its contents via module.name.
import module as alias Imports the module but binds it to a shorter or clearer local name.
from module import name Imports one specific attribute directly into the current namespace.
from module import a, b Imports multiple specific names in one statement.
from module import name as alias Imports one name under a different local name (avoids clashes).
from module import * Imports all public names directly — generally discouraged, see Common Mistakes.
import package.module Imports a submodule that lives inside a package.

Wherever a module lives on disk, Python has to be able to find it. When you run import x, Python searches, in order: the directory of the script being run, the directories listed in the PYTHONPATH environment variable, the standard library’s installation directories, and finally any site-packages directory where installed third-party packages live. This full search list is available at runtime as the list sys.path, and you can inspect or even modify it (though modifying it directly is rarely the right fix for import problems).

Examples

Example 1: Writing and importing your own module

Suppose you save the following as geometry.py:

"""geometry.py - simple geometric utility functions."""

PI = 3.14159


def area_circle(radius):
    return PI * radius ** 2


def area_rectangle(width, height):
    return width * height


if __name__ == "__main__":
    print("This runs only when geometry.py is executed directly.")

Now, in a different file saved in the same folder, you can import and use it:

import geometry

print(geometry.PI)
print(geometry.area_circle(5))
print(geometry.area_rectangle(4, 6))

Output:

3.14159
78.53975
24

Notice that everything defined in geometry.py — the constant PI and both functions — is reached through the geometry. prefix. The if __name__ == "__main__": block at the bottom of geometry.py never runs here, because geometry was imported, not executed directly; that guard exists precisely so a file can double as both a reusable module and a standalone script.

Example 2: Different ways to import the same functionality

import math
from math import sqrt, pow as power
import statistics as stats

print(math.sqrt(16))
print(sqrt(16))
print(power(2, 10))
print(stats.mean([1, 2, 3, 4]))

Output:

4.0
4.0
1024.0
2.5

All four lines ultimately call standard-library code, but each import style reaches it differently: math.sqrt uses the full module path, plain sqrt was pulled directly into the local namespace by the from import, power is math.pow under a chosen alias, and stats is the whole statistics module under a shorter name. All of these refer to the exact same underlying functions — only the name used to reach them changes.

Example 3: Modules are only executed once (import caching)

Save this as counter.py:

"""counter.py - demonstrates that modules are cached after the first import."""

print("counter.py is being loaded")

count = 0


def increment():
    global count
    count += 1
    return count

Then run:

import counter
import counter  # second import does NOT reload the module

print(counter.increment())
print(counter.increment())
print(counter.increment())

Output:

counter.py is being loaded
1
2
3

The line "counter.py is being loaded" prints only once, even though import counter appears twice, because the second import finds counter already sitting in sys.modules and simply reuses that object. The three calls to increment() also share one running total, proving every part of the program that imports counter is working with the exact same module-level count variable.

Under the Hood: What import Actually Does

When the interpreter hits an import x statement, it performs, in order:

  • Cache check — look up "x" in sys.modules. If it is already there, bind the name and stop; nothing else happens.
  • Finding — if not cached, search the locations in sys.path, in order, for a file or package matching that name.
  • Compiling — the source is compiled to bytecode. Python caches this bytecode in a __pycache__ folder as a .pyc file so it doesn’t have to recompile unchanged source on the next run.
  • Module object creation — a new, empty module object is created with attributes like __name__, __file__, and __spec__ already set.
  • Execution — the module’s code runs top to bottom inside that module’s own fresh namespace. Any function or class defined, or variable assigned, at the top level becomes an attribute of the module object.
  • Caching and binding — the finished module object is stored in sys.modules, and the importing code’s local name x is bound to it.

A from module import name statement still runs every one of these steps for the whole module — there is no way to import "only part" of a file’s execution — it just additionally copies one attribute out into the local namespace afterward. This is also the mechanism behind the very common if __name__ == "__main__": idiom: Python sets a module’s __name__ attribute to "__main__" only when that file is the one being run directly (as in python3 file.py); when the same file is imported from elsewhere, __name__ is set to its module name instead. Guarding demo code, tests, or a command-line entry point behind that check lets a file work both as a script and as an importable library.

Common Mistakes

Mistake 1: Wildcard imports silently shadow names

It’s tempting to write from module import * to avoid typing a prefix, but it dumps every public name from that module into your namespace — including ones that quietly replace builtins or names from other imports.

from math import *

print(pow(2, 3))
print(type(pow(2, 3)))

Output:

8.0
<class 'float'>

Many readers expect pow(2, 3) here to be the builtin pow(), which returns the int 8. Instead, from math import * replaced the builtin with math.pow, which always returns a float. This kind of invisible shadowing is exactly why wildcard imports are discouraged: nothing in the calling code tells you where a name actually came from. The fix is to import explicitly, keeping each name’s origin visible:

import math

print(pow(2, 3))
print(math.pow(2, 3))

Output:

8
8.0

Now pow is unambiguously the builtin (returns 8, an int) and math.pow is unambiguously the math-module version (returns 8.0, a float) — the prefix makes the distinction obvious at the call site.

Mistake 2: Circular imports

A circular import happens when two modules import each other, directly or indirectly, at the top level:

# file: a.py
import b

def func_a():
    return b.func_b() + 1

# file: b.py
import a

def func_b():
    return a.func_a() + 1

If something imports a first, Python starts executing a.py, hits import b, and starts executing b.py. But b.py immediately does import a — and a is already sitting in sys.modules, only it’s the half-finished version, since a.py hasn’t reached the line that defines func_a yet. Depending on exactly what each file needs from the other, this surfaces as an ImportError or an AttributeError complaining a name doesn’t exist yet. The cleanest fix is usually to restructure so the shared logic lives in a third module that both of the others import. When that isn’t practical, deferring the import until the function actually runs sidesteps the problem, because by the time the function is called, both modules have finished loading:

def func_a():
    import b  # imported lazily, only when func_a actually runs
    return b.func_b() + 1

Best Practices

  • Prefer import module or from module import specific_name over from module import *, so it’s always clear where a name comes from.
  • Follow PEP 8’s import ordering: standard-library imports first, then third-party packages, then your own local modules, each group separated by a blank line.
  • Guard script-only behavior with if __name__ == "__main__": so a file remains safely importable elsewhere.
  • Keep each module focused on one responsibility — a file that does five unrelated things is a sign it should be split.
  • Never name your own file the same as a standard-library or installed module (for example random.py or math.py) in a project directory; your file will shadow the real one and produce confusing errors.
  • Avoid circular imports by keeping shared code in a separate, lower-level module that others depend on, rather than having peer modules depend on each other.
  • If you must expose a wildcard-import-friendly module, define __all__ = ["name1", "name2"] at module level to explicitly control what from module import * actually pulls in.
  • Use absolute imports (from mypackage.utils import helper) over implicit relative ones for clarity, especially in packages meant to be reused.

Practice Exercises

  • Create a module named temperature.py containing two functions, celsius_to_fahrenheit(c) and fahrenheit_to_celsius(f). In a second file, import the module and print temperature.celsius_to_fahrenheit(0) and temperature.celsius_to_fahrenheit(100). Work out what each call should print before you check.
  • Two files in the same program both run import config. One of them sets config.DEBUG = True partway through startup. If the other file checks config.DEBUG afterward, will it see True? Explain your answer in terms of sys.modules.
  • A module called stringutils.py defines several helper functions, some of which happen to share names with builtins (like format or all). Write an import statement for stringutils that lets you call its functions with a short prefix while guaranteeing you never accidentally shadow a builtin.

Summary

  • A module is any .py file; its name is the filename without the extension.
  • import creates a real module object, executes the file once, and stores every top-level name as an attribute on it.
  • Already-imported modules are cached in sys.modules, so repeated imports reuse the same object instead of re-running the file.
  • sys.path is the ordered list of locations Python searches to find a module.
  • __name__ equals "__main__" only when a file is run directly, which is what powers the if __name__ == "__main__": idiom.
  • Avoid from module import * in real code — it can silently shadow builtins and other imports.
  • Circular imports come from two modules importing each other at the top level; fix them by restructuring shared code or deferring the import into a function.