What is a module?
A module is any .py file. When you write import math, Python finds math.py somewhere on its search path, executes it, and returns the result as a module object. Everything defined at the top level of that file becomes an attribute on the object.
1import math23print(math.pi) # 3.1415926535897934print(math.sqrt(16)) # 4.05print(math.floor(3.7)) # 367# The module is an object you can inspect8print(type(math)) # <class 'module'>
Your own modules
Any file you write is importable as a module. Save this as utils.py and import it from any file in the same directory.
1# utils.py2def add(a, b):3 return a + b45def clamp(value, lo, hi):6 return max(lo, min(value, hi))78TIMEOUT = 30 # module-level constant
1# main.py (same directory as utils.py)2import utils34print(utils.add(2, 3)) # 55print(utils.clamp(150, 0, 100)) # 1006print(utils.TIMEOUT) # 30
The __name__ guard
Every module has a built-in __name__ attribute. When Python runs a file directly, __name__ is set to "__main__". When the same file is imported by another module, __name__ is set to the module's name instead.
1# utils.py2def add(a, b):3 return a + b45if __name__ == "__main__":6 # This block only runs when executing utils.py directly.7 # It does NOT run when utils is imported.8 assert add(2, 3) == 59 assert add(-1, 1) == 010 print("All tests passed.")
This guard is the standard way to make a file work as both an importable library and a runnable script. Without it, any demo or test code at the bottom of your file runs every time someone imports the module.
Import syntax
Python has four import forms. Each serves a different purpose.
import module
Imports the whole module. You access names through the module object using dot notation. This keeps your local namespace clean and makes every name's origin obvious at a glance.
1import os2import json34home = os.path.expanduser("~")5data = json.loads('{"key": "value"}')67# The source is unambiguous, you always know where a name came from8print(os.getcwd()) # /home/user/project
from module import name
Imports specific names directly into your namespace. Use this when you need only one or two things from a large module and the context is clear without the module prefix.
1from math import sqrt, pi2from pathlib import Path3from datetime import datetime, timedelta45print(sqrt(25)) # 5.06print(pi) # 3.14159265358979378# Multiple imports from the same module can span lines9from os.path import (10 join,11 exists,12 dirname,13)
import module as alias
Aliases shorten long module names. The data science ecosystem has settled on a few conventions, use them so your code looks familiar to everyone else.
1import numpy as np # universal convention2import pandas as pd # universal convention3import matplotlib.pyplot as plt4import datetime as dt # useful when the full name is repetitive56arr = np.array([1, 2, 3])7today = dt.date.today()
Wildcard imports, avoid them
from module import * dumps every public name from the module into your current namespace. This makes it impossible to tell where any name came from, and it silently overwrites names that were already defined.
1# Bad, where does chain come from? Where does sqrt come from?2from itertools import *3from math import *45result = chain([1, 2], [3, 4]) # itertools? math? your own code?6val = sqrt(9) # ambiguous78# Good, every source is explicit9from itertools import chain10from math import sqrt
The only accepted use of wildcard imports is inside __init__.py to re-export a curated public API, controlled by __all__.
Packages
A package is a directory that contains an __init__.py file. That file marks the directory as a package and can be empty or expose the package's public API. Packages let you organize related modules into a hierarchy that mirrors your project's structure.
1# A typical project layout2myapp/3 __init__.py # makes myapp a package4 models.py5 utils.py6 api/7 __init__.py # makes api a nested package8 routes.py9 auth.py
1# Importing from a package2import myapp.models # full dotted path3from myapp import utils # module from a package4from myapp.api import routes # module from a nested package5from myapp.api.auth import verify_token # name from a nested module
__init__.py as a public API
When someone imports your package, they see what __init__.py exposes. Use it to pull the most important names up to the top level so callers don't need to know your internal file structure.
1# myapp/__init__.py2from .models import User, Product # re-export from submodules3from .utils import format_currency45# Now callers can write:6from myapp import User7# instead of navigating the internals:8from myapp.models import User
__all__ controls what gets exported
__all__ is a list of names that from module import * will include. Even if you never use wildcard imports yourself, it serves as documentation: it tells readers which names are part of the public interface.
1# myapp/utils.py2__all__ = ["format_currency", "parse_date"] # public API34def format_currency(amount, symbol="$"):5 return f"{symbol}{amount:.2f}"67def parse_date(s):8 from datetime import datetime9 return datetime.strptime(s, "%Y-%m-%d")1011def _build_query(params): # underscore = private by convention12 pass
Namespace packages
Python 3.3 and later support namespace packages: directories without __init__.py. They are useful for splitting one logical package across multiple directories or repositories. For ordinary projects, always include __init__.py. It is explicit and avoids surprising behavior.
Relative imports
Inside a package, you can import from sibling modules using dots. One dot means the current package. Two dots mean the parent package. Relative imports only work inside packages, not in scripts you run directly.
1# Project structure:2# myapp/3# __init__.py4# models.py5# utils.py6# api/7# __init__.py8# routes.py9# auth.py1011# Inside myapp/api/routes.py:12from . import auth # sibling module in same package13from .auth import verify_token # specific name from sibling1415from .. import models # module in parent package16from ..utils import format_date # specific name from parent module
Absolute vs relative, which to prefer
Absolute imports are explicit and work from anywhere. Relative imports are shorter inside deeply nested packages. The Python community generally prefers absolute imports, they survive renaming and restructuring without breaking.
1# myapp/api/routes.py23# Absolute, always clear, always works4from myapp.models import User5from myapp.utils import format_date67# Relative, shorter, but breaks if you move or rename the package8from ..models import User9from ..utils import format_date
If you run a file directly and it uses relative imports, Python raises ImportError: attempted relative import with no known parent package. Relative imports require the file to be part of an installed package, not a standalone script.
Circular imports
A circular import occurs when module A imports B, and B imports A. Python partially executes A before finishing B's import of A, which gives B an incomplete view of A. The result is usually an ImportError or a missing attribute at runtime.
1# a.py, imports from b2from b import greet34def name():5 return "Alice"67# b.py, imports from a (circular!)8from a import name910def greet():11 return f"Hello, {name()}"12# ImportError: cannot import name 'name' from partially initialized module 'a'
Fix circular imports by moving shared code into a third module that both A and B import, or by deferring the import inside the function where it is actually used.
1# shared.py, neither a.py nor b.py imports the other2def name():3 return "Alice"45# b.py, imports only from shared6from shared import name78def greet():9 return f"Hello, {name()}"1011# a.py, imports only from shared and b12from shared import name13from b import greet1415print(greet()) # Hello, Alice
How Python finds modules
When you write import utils, Python searches a list of directories called sys.path in order. It imports the first match it finds. If nothing matches, you get ModuleNotFoundError.
1import sys23for path in sys.path:4 print(path)
Python searches in this order:
- The directory containing the script being run (or the current directory in the REPL)
- Directories listed in the
PYTHONPATHenvironment variable - Standard library directories
- Site-packages (where pip installs third-party packages)
Why ModuleNotFoundError happens
The most common causes:
1# 1. Package not installed in this environment2import requests # fix: pip install requests34# 2. Wrong virtual environment, always check which Python you're using5import sys6print(sys.executable) # shows the exact binary7# /home/user/wrong-project/.venv/bin/python89# 3. Module file is in the wrong location relative to the script10# script at: project/scripts/run.py11# module at: project/utils.py12# Python only looks in project/scripts/, not project/13# fix: structure as a package and install it properly
Never modify sys.path in production code. Use a proper package structure and install it in development mode with pip install -e . so Python finds it the right way.
Module caching
Python caches every imported module in sys.modules. Importing the same module a second time does not re-execute its code, it returns the cached object immediately. This is why import side effects only run once.
1import sys2import math34# Already cached from the import above5print("math" in sys.modules) # True6print(sys.modules["math"] is math) # True, same object, not a copy