Somewhere around line 800, a single-file project stops being convenient and starts being a maze. Python modules are the way out: split the code into focused .py files and import what you need, where you need it. This guide covers creating and importing modules, the __name__ == ‘__main__’ guard, and exactly how Python locates your code.
“Simplicity is prerequisite for reliability.”
Edsger Dijkstra
Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Beginner | Reading Time: 17 minutes
Here is a fact that surprises a lot of beginners: every Python file you have ever written is already a module. That utils.py sitting in your project is a module. The math you import to get pi is a module. The little script you wrote last week to scrape job listings is a module too. The word “module” just means “a .py file you can import.” Nothing fancier than that.
Think of a module like a labelled drawer in your kitchen. One drawer holds the spoons, another holds the knives, another holds the spices. When you need a spoon, you open the spoon drawer. You do not dump everything into one giant drawer and dig through it. A Python import is you opening the right drawer and reaching in for exactly the tool you need.
The moment you type import, Python quietly does a lot of work: it figures out where the file lives, runs it once, compiles it to bytecode, and caches it so the next import is instant. Once you understand how import, from ... import, and import ... as actually behave, the mysterious errors stop. No more circular import surprises, no more name clashes, no more “wait, where did this function even come from.” This post walks through all of it, one tested example at a time.
Table of Contents
The Simplest Module You Can Make
A module is just a .py file. Create one, import it from another file, done. In the example below, greetings.py holds the reusable functions, and main.py imports them to greet two users named Aditi and Aviraj.
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
The flowchart traces how Python resolves a module import. First it checks the sys.modules cache and skips all the work if the module is already loaded. If not, it checks the built-in modules compiled into the interpreter, and only then searches the sys.path directories one by one, in order, until it finds the file. This is exactly why the first import of a big library feels slow but every import after that is instant: the first run does the loading, the rest just grab the cached copy. It also explains why sys.path order matters when two directories happen to hold a module with the same name. The examples below show how to watch and control this whole process.
📄 greetings.py: a simple module
"""Greetings module: reusable greeting functions."""
def hello(name):
return f"Hello, {name}!"
def formal_hello(name, title="Mr."):
return f"Good day, {title} {name}."
PI = 3.14159
TEAM = ["Rahul", "Niranjan", "Viraj"]
📄 main.py: importing and using the module
import greetings
print(greetings.hello("Aditi"))
print(greetings.formal_hello("Aviraj", "Dr."))
print(f"PI = {greetings.PI}")
print(f"Team: {greetings.TEAM}")
▶ Output
Hello, Aditi! Good day, Dr. Aviraj. PI = 3.14159 Team: ['Rahul', 'Niranjan', 'Viraj']
What happened here: import greetings tells Python to find greetings.py, run it from top to bottom, and hand you everything inside it through the greetings name. You then reach in with dot notation, like greetings.hello(). Notice the file runs only once. If you import greetings again from another file later, Python does not re-run it. It just gives you the cached copy it already has sitting in sys.modules. That is the kitchen drawer again: you opened it once, and now it stays open for the rest of the program.
Import Variations: Five Ways to Import
Importing Python modules is a bit like borrowing from a library. You can carry home the whole encyclopedia set (import math), photocopy just the one page you need (from math import sqrt), or slap a short label on a thick book so it is easier to carry (import statistics as stats). Each style trades convenience for clarity in a different way, and the example below runs all of them side by side.
📄 import_styles.py: all import forms compared
# Style 1: Import the whole module
import math
print(math.sqrt(16)) # 4.0
# Style 2: Import specific names
from math import sqrt, pi
print(sqrt(25)) # 5.0
print(pi) # 3.141592653589793
# Style 3: Import with alias
import statistics as stats
scores = [88, 92, 79, 95, 84]
print(stats.mean(scores)) # 87.6
# Style 4: Import specific name with alias
from collections import defaultdict as dd
word_count = dd(int)
for word in ["python", "is", "python"]:
word_count[word] += 1
print(dict(word_count)) # {'python': 2, 'is': 1}
# Style 5: Import everything (avoid this!)
# from math import *
# This dumps every name into your file, so you lose track of where things came from
▶ Output
4.0
5.0
3.141592653589793
87.6
{'python': 2, 'is': 1}
What happened here: Each style has its moment. import math is the safest, because every call is clearly labelled with where it came from, like math.sqrt. from math import sqrt saves you typing when you lean on one function over and over. An alias like import numpy as np is a community handshake: every data scientist on earth reads np as NumPy, so the short name actually makes the code clearer, not muddier. The one to skip is the wildcard from X import *. It pours every name from that module straight into your file, and three months later you will be staring at sqrt with no idea which module it came from. Leave the star imports alone.
The __name__ == "__main__" Guard
Here is a situation every Python developer runs into. You write a module full of handy functions, and at the bottom you add a few lines to test them. That is great while you are building it. But the day someone imports your module to use those functions, they do not want your test code firing off in their face. The __name__ guard is the clean little switch that solves this.
Think of it like the light in your fridge. The bulb only turns on when you open the door. Shut the door and it goes dark, even though the fridge is still humming away. The code under the guard is that bulb: it lights up only when you run the file directly, and stays dark when the file is imported.
📄 calculator.py: module with the __name__ guard
"""Calculator module with basic operations."""
def add(a, b):
return a + b
def multiply(a, b):
return a * b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# This block ONLY runs when you execute calculator.py directly
# It does NOT run when someone imports this module
if __name__ == "__main__":
print("Running calculator tests...")
print(f"add(3, 5) = {add(3, 5)}")
print(f"multiply(4, 7) = {multiply(4, 7)}")
print(f"divide(10, 3) = {divide(10, 3):.2f}")
try:
divide(1, 0)
except ValueError as e:
print(f"Caught: {e}")
▶ Output (when run directly: python calculator.py)
Running calculator tests... add(3, 5) = 8 multiply(4, 7) = 28 divide(10, 3) = 3.33 Caught: Cannot divide by zero
📄 app.py: importing calculator (test code does NOT run)
from calculator import add, multiply
budget = 50000
bonus = 7500
total = add(budget, bonus)
print(f"Anvi's total compensation: ₹{total:,}")
hourly_rate = 250
hours = 160
monthly = multiply(hourly_rate, hours)
print(f"Anvay's monthly: ₹{monthly:,}")
▶ Output
Anvi's total compensation: ₹57,500 Anvay's monthly: ₹40,000
What happened here: Python gives every file a hidden variable called __name__. When you run a file directly, like python calculator.py, Python sets __name__ to the string "__main__". But when that same file gets imported, __name__ becomes the module name instead, in this case "calculator". So the line if __name__ == "__main__": is really asking one simple question: “am I the file being run, or am I being imported?” The test block runs only in the first case.
In app.py we imported calculator to work out the pay of two employees, Anvi and Anvay, and the guard stayed shut: not a single test line printed. This one little pattern is the most important habit in writing reusable Python modules.
How Python Finds Your Modules
When you write import foo, Python does not search your whole computer. It looks in a fixed list of places, in a fixed order, and stops at the first match. Once you know that order, the dreaded ModuleNotFoundError stops being a mystery. It is a lot like Python checking your contacts list: it scans the entries from top to bottom and grabs the first one that matches the name.
📄 import_path.py: where Python looks for modules
import sys
# 1. Already-imported modules are cached here
print("Cached modules:", len(sys.modules))
print("'os' cached?", 'os' in sys.modules)
# 2. sys.path: the search order
print("\nPython searches these paths (in order):")
for i, path in enumerate(sys.path[:6]):
print(f" {i}: {path}")
print(f" ... ({len(sys.path)} total paths)")
# 3. You can add paths dynamically
sys.path.append("/home/rahul/my_libs")
print(f"\nPaths after append: {len(sys.path)}")
▶ Output (counts and paths will vary on your machine)
Cached modules: 36 'os' cached? True Python searches these paths (in order): 0: /home/rahul/projects 1: /usr/lib/python314.zip 2: /usr/lib/python3.14 3: /usr/lib/python3.14/lib-dynload 4: /home/rahul/.local/lib/python3.14/site-packages 5: /usr/lib/python3.14/site-packages ... (7 total paths) Paths after append: 8
What happened here: Python checks the sys.modules cache first, so anything already loaded (like os, which is why it shows up as cached) skips the search entirely. For everything else it walks sys.path top to bottom: the directory your script lives in comes first, then the standard library folders, and finally site-packages where pip drops the libraries you install. The exact paths and counts on your screen will look different from the ones above, and that is completely normal.
They depend on your operating system and where Python is installed. When you hit ModuleNotFoundError, it almost always means one of three things: the file is not on any of these paths, you misspelled the module name, or you are in the wrong virtual environment. And since sys.path is just a regular Python list, you can add your own folder to it at runtime, which is exactly what the append line did.
Module Attributes: The Hidden Metadata
Every module quietly carries a little ID card. Python attaches a few special attributes to it, all wrapped in double underscores, that tell you the module’s name, where it lives, and what it is for. Here is a real surprise tucked inside this section too: some modules have no file at all, because they are compiled straight into Python.
📄 module_attributes.py: every module carries built-in metadata
import math
import statistics
import os
# Every module has a name and a docstring
print(f"Module name: {math.__name__}")
print(f"Module doc: {math.__doc__[:52]}...")
# Built-in modules (compiled into Python) have no file on disk
print(f"Is math built-in? {math.__spec__.origin == 'built-in'}")
# A pure-Python module lives in a real .py file
print(f"statistics name: {statistics.__name__}")
print(f"statistics file: {os.path.basename(statistics.__file__)}")
# dir() lists everything a module exposes
math_names = [x for x in dir(math) if not x.startswith('_')]
print(f"\nPublic names in math: {len(math_names)}")
print(f"First 10: {math_names[:10]}")
# hasattr() checks for a name before you use it
print(f"\nHas 'sqrt'? {hasattr(math, 'sqrt')}")
print(f"Has 'cube'? {hasattr(math, 'cube')}")
▶ Output
Module name: math Module doc: This module provides access to the mathematical func... Is math built-in? True statistics name: statistics statistics file: statistics.py Public names in math: 62 First 10: ['acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'cbrt', 'ceil', 'comb'] Has 'sqrt'? True Has 'cube'? False
What happened here: Every module exposes a few built-in attributes. __name__ is its name and __doc__ is its docstring, the text right at the top of the file. Here is the part most tutorials skip: math is a built-in module, compiled right into the Python interpreter, so it has no .py file at all. Try to read math.__file__ and you get an AttributeError. That is why the code checks __spec__.origin == 'built-in' instead.
A pure-Python module like statistics, on the other hand, really does live in a statistics.py file, and __file__ points right at it. The other two helpers are your exploration toolkit: dir(module) lists everything the module offers, and hasattr(module, name) tells you whether a name exists before you reach for it.
The Catch: Circular Imports
This is the one that bites every Python developer sooner or later. Module A imports Module B, and Module B turns around and imports Module A. You might expect an infinite loop, but Python does not spin forever. It does something sneakier: it hands one of the modules over half-finished, before all its functions are defined.
Picture two friends each waiting for the other to go first. Rahul says “I will leave once Niranjan leaves,” and Niranjan says “I will leave once Rahul leaves.” Nobody moves. A circular import is that same standoff: each file is paused partway through, waiting on a name the other has not defined yet.
📄 user_service.py: the circular import problem
# user_service.py
from notification_service import send_welcome_email # imports notification
def create_user(name, email):
user = {"name": name, "email": email}
send_welcome_email(user)
return user
📄 notification_service.py: imports user_service back!
# notification_service.py
from user_service import create_user # imports user_service back: CIRCULAR!
def send_welcome_email(user):
print(f"Sending welcome email to {user['email']}")
def send_reminder():
user = create_user("Rahul", "rahul@technoscripts.com")
print(f"Reminder sent to {user['name']}")
▶ Output (when running user_service.py on Python 3.14.6; your paths will differ)
Traceback (most recent call last):
File "C:\Users\Rahul\projects\user_service.py", line 2, in <module>
from notification_service import send_welcome_email # imports notification
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\Rahul\projects\notification_service.py", line 2, in <module>
from user_service import create_user # imports user_service back: CIRCULAR!
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\Rahul\projects\user_service.py", line 2, in <module>
from notification_service import send_welcome_email # imports notification
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ImportError: cannot import name 'send_welcome_email' from 'notification_service' (consider renaming 'C:\\Users\\Rahul\\projects\\notification_service.py' if it has the same name as a library you intended to import)
📄 fix_circular.py: move the import inside the function
# notification_service.py (FIXED)
def send_welcome_email(user):
print(f"Sending welcome email to {user['email']}")
def send_reminder():
# Import inside the function: runs only when called, not at load time
from user_service import create_user
user = create_user("Rahul", "rahul@technoscripts.com")
print(f"Reminder sent to {user['name']}")
What happened here: Python starts running user_service from the top. Line one says “import from notification_service,” so Python pauses user_service and jumps over to run notification_service instead. But line one of that file says “import from user_service,” which is still paused and only half-built. The name it needs does not exist yet, so the whole thing falls over with an ImportError. On Python 3.14.6 the message even nudges you to check for a filename clash, since a same-name file is a common cause.
The fix is the lazy import you saw above: move one import inside a function. Code inside a function does not run until the function is actually called, and by that time both modules have finished loading, so the name is sitting there ready. Same idea as our two friends: once one of them just walks out the door, the standoff is over.
When You’ll Use Modules
You will not have to go looking for reasons to use Python modules. The moment a project grows past a single short file, they show up on their own. Here are the three everyday situations where reaching for a module just feels right.
- Code reuse: Put your validation functions in
validators.py, then import them in every route handler. Fix a bug once and the fix lands everywhere at once. - Organization: A 2000-line
app.pyis unmaintainable. Split it intomodels.py,views.py,utils.py. Each file has a single responsibility. - Testing: When code is in a module, you can import it in test files and test individual functions without running the whole application.
Common Mistakes
❌ Mistake 1: Naming your file the same as a standard library module
# If you name your file "random.py" and then: import random print(random.randint(1, 10)) # Python imports YOUR random.py instead of the standard library! # AttributeError: module 'random' has no attribute 'randint'
✅ Fix: Never name your files after standard library modules
# Rename your file to something else: my_random.py, random_utils.py # Common offenders: random.py, email.py, test.py, string.py, collections.py
❌ Mistake 2: Using from module import * in production code
from os import * # Dumps 100+ names into your file at once from sys import * # Overwrites any names that clash! # Good luck figuring out where 'path' came from later
✅ Fix: Import specific names or use the module prefix
import os import sys # Clear and explicit: os.path vs sys.path, no ambiguity
❌ Mistake 3: Forgetting that module code runs on import
# bad_module.py
print("This runs when someone imports me!") # Side effect on import
connection = connect_to_database() # Expensive operation on import
# Anyone who writes "import bad_module" triggers a DB connection
✅ Fix: Put side effects behind the __name__ guard
# good_module.py
def get_connection():
return connect_to_database()
if __name__ == "__main__":
conn = get_connection()
print("Connected!")
Conclusion
Python modules are how you keep a growing project from turning into one giant tangled file. Every .py file is a module, and the day you write your first import you are already using one. The different import styles let you decide exactly what lands in your file, the __name__ guard keeps your test code from firing when someone imports you, and a quick peek at sys.path tells you precisely where Python goes looking. Get comfortable with these four ideas and most import errors simply stop happening.
One module is plenty for a small project. Once you have a dozen Python modules, you will want to group them into folders, and that is exactly what packages are for. In the packages tutorial, you will learn how to organize modules into directory trees with __init__.py, relative imports, and sub-packages. And if you want to browse every post in this series in one place, head over to the Python + AI/ML tutorial series home.
Frequently Asked Questions
What is a Python module?
A module is any .py file. When you import greetings, Python looks for greetings.py, executes it, and makes its contents available through the greetings namespace. Python modules are nothing more special than that: every file you write can be imported.
What does __name__ == ‘__main__’ mean?
When Python runs a file directly (python script.py), it sets __name__ to '__main__'. When the file is imported as a module, __name__ becomes the module name. The guard if __name__ == '__main__': lets you include test/demo code that only runs during direct execution, not on import.
How does Python find modules when you import them?
Python searches in order: 1) the sys.modules cache for anything already imported, 2) built-in modules compiled into Python (such as sys and math), 3) the directories in sys.path, which start with your script’s folder, then the standard library, then site-packages where pip installs things. If nothing matches anywhere, it raises ModuleNotFoundError.
What is a circular import and how do I fix it?
A circular import happens when module A imports module B, and module B imports module A. Python pauses one of them half-built and the name you need is not defined yet, so you get an ImportError. On Python 3.14.6 the message also suggests checking for a filename clash. Fix it by moving one import inside a function (a lazy import), restructuring to remove the cycle, or moving the shared code into a third module.
Should I use import math or from math import sqrt?
import math is safer because every call says where it came from, like math.sqrt. Use from math import sqrt when you lean on one function heavily and its source is obvious. Follow community conventions like import numpy as np and import pandas as pd. Never use from module import * in production code.
Interview Questions on Python Modules
The same ideas as they show up in real interviews, framed as scenarios you can practice out loud.
Q: A teammate imports your module and their console suddenly fills with print output, and a database connection opens that they never asked for. What went wrong, and how do you fix it?
The module has side effects at the top level: print calls and a database connection sitting outside any function. Python executes every top-level line of a module the first time it is imported, so importers trigger all of it. The fix is to wrap the expensive work in functions and put any demo or test code behind if __name__ == "__main__":, so it only runs when the file is executed directly.
Q: Your script suddenly fails with “AttributeError: module ‘random’ has no attribute ‘randint'”. The same code worked yesterday. What do you check first?
Check whether a file named random.py was added to the project folder. The script’s own directory sits at the front of sys.path, so a local file shadows the standard library module of the same name. Print random.__file__ (or random.__spec__.origin) to see which file actually got imported. Rename the local file and delete any stale .pyc files in __pycache__.
Q: If ten different files in a project all write import requests, does Python execute the requests module ten times?
No, it executes once. The first import runs the module and stores the resulting module object in sys.modules. Every later import, from any file in the same process, just returns that cached object, which is why repeat imports are nearly free. If you genuinely need to re-execute a module (rare outside interactive sessions), importlib.reload() exists for that.
Q: Your code works on your laptop but throws ModuleNotFoundError for an installed package on the server. Walk through your debugging steps.
First confirm which Python is actually running: print sys.executable. Nine times out of ten the package was pip-installed into a different environment than the one running the app. Then print sys.path and check whether the package’s site-packages directory is in it. Fix by activating the right virtual environment or installing with that exact interpreter: python -m pip install package.
Q: Your Command-Line Interface (CLI) tool takes three seconds just to print its help text because it imports pandas at the top. How do you speed up startup without removing the feature that needs pandas?
Use a lazy import: move import pandas from the top of the file into the one function that actually uses it. Code inside a function body does not run until the function is called, so the help path never pays the import cost. Thanks to the sys.modules cache, calling that function repeatedly still imports pandas only once. This is the same technique used to break circular imports.
Q: How can you tell whether a module is built into the interpreter or lives as a .py file on disk?
Check module.__spec__.origin: built-in modules like sys and math report the string 'built-in' because they are compiled into the Python binary. A pure-Python module like statistics has a real path there, and also exposes __file__. In fact, accessing __file__ on a built-in module raises AttributeError, which is a common catch in code that tries to locate modules on disk.
Try It Yourself
Create a module called string_utils.py with functions: reverse(text), word_count(text), and truncate(text, max_length). Add a __name__ guard with test cases. Import it from a separate main.py and verify the test code doesn’t run on import.
Go deeper: the official Python documentation covers every edge case of this topic.
Related Posts
Previous: Code Review in Python: Reading and Improving Other People’s Code
Next: Python: Packages, __init__.py, Relative Imports
Series Home: Python + AI/ML Tutorial Series

No comment