One of the most confusing aspects of Python for newcomers - and even experienced developers - is the import system. You’ve probably seen those cryptic ImportError: attempted relative import with no known parent package messages and wondered what you did wrong.
The truth is, Python’s import system is elegant once you understand it. Let’s demystify absolute imports, relative imports, and project structure best practices.
Before diving into imports, let’s talk about why a good project structure is crucial:
Readability - Others (including future you) can navigate your code easily
Scalability - Adding features doesn’t turn your project into spaghetti
Reusability - Modules can be imported and reused across projects
Collaboration - Team members know where things belong
Import clarity - Python’s import system relies on logical structure
Here’s a well-organized project structure we’ll use as reference:
my_project/
├── __init__.py # Makes my_project a package
├── main.py # Entry point
├── requirements.txt # Dependencies
├── README.md
├── setup.py # For packaging (optional)
├── package_a/
│ ├── __init__.py
│ ├── module_a1.py
│ └── module_a2.py
└── package_b/
├── __init__.py
├── module_b1.py
└── subpackage_c/
├── __init__.py
└── module_c1.py
The Magic of __init__.py#
That __init__.py file is crucial. Its presence tells Python: “This directory is a package, not just a folder.”
The file can be empty, but it can also:
Initialize package-level variables
Import specific modules for easier access
Define
__all__forfrom package import *behavior
Example __init__.py:
# my_project/package_a/__init__.py
# Import frequently used functions into package namespace
from .module_a1 import greet_a1
from .module_a2 import extended_greet
# Package-level constant
VERSION = "1.0.0"
# Control what * imports
__all__ = ['greet_a1', 'extended_greet', 'VERSION']
Now you can do:
from package_a import greet_a1 # Instead of package_a.module_a1.greet_a1
When you write import something, Python searches in this order:
Built-in modules (like
os,sys)Directories in
sys.path:The directory containing the script you’re running
Directories in
PYTHONPATHenvironment variableStandard library directories
Site-packages (installed packages)
You can see your path:
import sys
print(sys.path)
Absolute imports specify the full path from your project root or from sys.path.
Example Setup#
my_project/package_a/module_a1.py:
def greet_a1():
print("Hello from module_a1!")
class HelperClass:
def __init__(self, name):
self.name = name
my_project/package_b/module_b1.py:
def perform_task():
print("Performing task in module_b1!")
my_project/main.py:
# Absolute imports - clear and explicit
from package_a import module_a1
from package_b import module_b1
def run():
print("Running main application...")
module_a1.greet_a1()
module_b1.perform_task()
helper = module_a1.HelperClass("Tashif")
print(f"Helper name: {helper.name}")
if __name__ == "__main__":
run()
Running the Code#
From the directory above my_project:
python my_project/main.py
Or from inside my_project:
python main.py
Why Absolute Imports Rock#
✅ Crystal clear - No ambiguity about where modules come from
✅ Safe refactoring - Renaming parent packages won’t break imports
✅ IDE friendly - Better autocomplete and navigation
✅ Explicit is better than implicit - The Zen of Python
Relative imports use dots (.) to navigate the package hierarchy, like Unix paths:
.= current package..= parent package...= grandparent package
When to Use Them#
Relative imports shine for imports within the same package. They make code more portable when you rename or move packages.
Examples#
my_project/package_a/module_a2.py:
# Import from sibling module in same package
from .module_a1 import greet_a1, HelperClass
def extended_greet():
greet_a1()
print("Extended greeting from module_a2!")
helper = HelperClass("Khan")
print(f"Helper: {helper.name}")
my_project/package_b/module_b1.py:
# Import from subpackage
from .subpackage_c.module_c1 import perform_subtask
def perform_task():
print("Task in module_b1")
perform_subtask()
my_project/package_b/subpackage_c/module_c1.py:
# Import from parent package using ..
from ..module_b1 import perform_task
def perform_subtask():
print("Subtask in module_c1")
# Be careful - this would create circular import:
# perform_task()
Here’s the trap that catches everyone: You cannot run a module with relative imports directly as a script.
This will fail:
python my_project/package_a/module_a2.py
# ImportError: attempted relative import with no known parent package
Why? When you run a Python file directly, it becomes __main__, not part of a package. It has no “parent package” to reference with . or ...
The Solution#
Always run your entry point (like main.py), which uses absolute imports:
# my_project/main.py
from package_a.module_a2 import extended_greet
if __name__ == "__main__":
extended_greet()
If you need to run a module within a package, use the -m flag:
# From project root
python -m my_project.package_a.module_a2
This tells Python to run the module as part of its package, so relative imports work.
✅ DO#
Use absolute imports in entry points (
main.py, test files)Use relative imports within packages for intra-package dependencies
Keep
__init__.pyin every package directoryUse virtual environments (always!)
Structure logically - group related functionality
Make one clear entry point for your application
❌ DON’T#
Don’t run modules with relative imports directly
Don’t use relative imports across different top-level packages
Don’t go up too many levels (
...and beyond gets confusing)Don’t manipulate
sys.pathunless absolutely necessaryDon’t use
from module import *in production code
Want to install your package with pip? Create a setup.py:
# my_project/setup.py
from setuptools import setup, find_packages
setup(
name="my_project",
version="1.0.0",
packages=find_packages(),
install_requires=[
# Your dependencies
],
entry_points={
'console_scripts': [
'my-app=my_project.main:run',
],
},
)
Then install in development mode:
pip install -e .
Now you can import your package from anywhere:
from my_project.package_a import module_a1
You mentioned it looks like “making an object and calling it” - great intuition! Let’s explore what’s really happening.
Modules Are Objects#
When you import my_module, Python:
Searches for
my_module.pyinsys.pathCreates a module object (instance of
types.ModuleType)Executes the module’s code in that object’s namespace
Caches the module object in
sys.modules
import sys
import my_module
# The module is an object!
print(type(my_module)) # <class 'module'>
print(sys.modules['my_module']) # Same object
Packages Are Module Objects Too#
When you import package_a, Python:
Finds
package_a/__init__.pyCreates a module object for
package_aExecutes
__init__.pyin that namespaceStores it in
sys.modules['package_a']
So when you write:
from package_a import module_a1
module_a1.greet_a1()
You’re accessing attributes on module objects, just like accessing methods on class instances!
Modules vs Classes#
The difference is:
Modules are namespaces for organizing code (singletons per import)
Classes are blueprints for creating multiple instances
# Module - only one instance
import math
import math as math2
print(math is math2) # True - same object
# Class - multiple instances
class MyClass:
pass
obj1 = MyClass()
obj2 = MyClass()
print(obj1 is obj2) # False - different objects
Let’s build a practical project structure:
blog_project/
├── __init__.py
├── main.py
├── requirements.txt
├── config.py
├── models/
│ ├── __init__.py
│ ├── user.py
│ └── post.py
├── database/
│ ├── __init__.py
│ └── connection.py
├── api/
│ ├── __init__.py
│ ├── routes.py
│ └── middleware.py
└── utils/
├── __init__.py
├── validators.py
└── helpers.py
main.py:
# Absolute imports from our packages
from config import DATABASE_URL
from database.connection import init_db
from api.routes import setup_routes
from models import User, Post # Exposed in models/__init__.py
def main():
init_db(DATABASE_URL)
app = setup_routes()
app.run()
if __name__ == "__main__":
main()
models/__init__.py:
# Expose models at package level
from .user import User
from .post import Post
__all__ = ['User', 'Post']
models/post.py:
# Relative import from same package
from .user import User
class Post:
def __init__(self, author: User, content: str):
self.author = author
self.content = content
api/routes.py:
# Absolute imports from other packages
from models import User, Post
from utils.validators import validate_email
# Relative import from same package
from .middleware import auth_required
def setup_routes():
# Route setup logic
pass
“No module named X”#
Problem: Python can’t find your module
Solutions:
Check you’re running from the right directory
Verify
__init__.pyexists in package directoriesCheck module spelling and path
Ensure package is in
sys.path
“Attempted relative import with no known parent package”#
Problem: Running a module with relative imports directly
Solution: Run the entry point instead, or use python -m package.module
Circular imports#
Problem: Module A imports B, B imports A
Solution:
Restructure to break the cycle
Move imports inside functions
Use forward references (type hints with quotes)
Python’s import system is powerful once you understand the patterns:
Absolute imports for clarity and entry points
Relative imports for intra-package convenience
__init__.pyto define packagesModules are objects that get cached
Structure logically for maintainability
Follow these principles, and you’ll have clean, maintainable Python projects that scale beautifully.
Now go structure that project like a pro! 🐍
Discussion
0Leave a comment