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"insys.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.pycfile 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 namexis 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 moduleorfrom module import specific_nameoverfrom 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.pyormath.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 whatfrom 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.pycontaining two functions,celsius_to_fahrenheit(c)andfahrenheit_to_celsius(f). In a second file, import the module and printtemperature.celsius_to_fahrenheit(0)andtemperature.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 setsconfig.DEBUG = Truepartway through startup. If the other file checksconfig.DEBUGafterward, will it seeTrue? Explain your answer in terms ofsys.modules. - A module called
stringutils.pydefines several helper functions, some of which happen to share names with builtins (likeformatorall). Write an import statement forstringutilsthat lets you call its functions with a short prefix while guaranteeing you never accidentally shadow a builtin.
Summary
- A module is any
.pyfile; its name is the filename without the extension. importcreates 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.pathis 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 theif __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.
