Dashboard
Python Low Level

Python Imports and Project Structure: A Practical Guide

Tashif Ahmad Khan August 30, 2025 8 min read 43 views 2 likes

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.

Why Project Structure Matters#

Before diving into imports, let’s talk about why a good project structure is crucial:

  1. Readability - Others (including future you) can navigate your code easily

  2. Scalability - Adding features doesn’t turn your project into spaghetti

  3. Reusability - Modules can be imported and reused across projects

  4. Collaboration - Team members know where things belong

  5. Import clarity - Python’s import system relies on logical structure

A Typical Python Project Structure#

Here’s a well-organized project structure we’ll use as reference:

text
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__ for from package import * behavior

Example __init__.py:

python
# 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:

python
from package_a import greet_a1  # Instead of package_a.module_a1.greet_a1

Understanding Python’s Import Search Path#

When you write import something, Python searches in this order:

  1. Built-in modules (like os, sys)

  2. Directories in sys.path:

    • The directory containing the script you’re running

    • Directories in PYTHONPATH environment variable

    • Standard library directories

    • Site-packages (installed packages)

You can see your path:

python
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:

python
def greet_a1():
    print("Hello from module_a1!")

class HelperClass:
    def __init__(self, name):
        self.name = name

my_project/package_b/module_b1.py:

python
def perform_task():
    print("Performing task in module_b1!")

my_project/main.py:

python
# 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:

bash
python my_project/main.py

Or from inside my_project:

bash
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: For Intra-Package Use#

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:

python
# 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:

python
# 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:

python
# 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()

The Relative Import Gotcha#

Here’s the trap that catches everyone: You cannot run a module with relative imports directly as a script.

This will fail:

bash
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:

python
# my_project/main.py
from package_a.module_a2 import extended_greet

if __name__ == "__main__":
    extended_greet()

Using -m Flag for Package Modules#

If you need to run a module within a package, use the -m flag:

bash
# 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.

Best Practices Cheatsheet#

✅ DO#

  1. Use absolute imports in entry points (main.py, test files)

  2. Use relative imports within packages for intra-package dependencies

  3. Keep __init__.py in every package directory

  4. Use virtual environments (always!)

  5. Structure logically - group related functionality

  6. Make one clear entry point for your application

❌ DON’T#

  1. Don’t run modules with relative imports directly

  2. Don’t use relative imports across different top-level packages

  3. Don’t go up too many levels (... and beyond gets confusing)

  4. Don’t manipulate sys.path unless absolutely necessary

  5. Don’t use from module import * in production code

Advanced: Making Your Package Installable#

Want to install your package with pip? Create a setup.py:

python
# 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:

bash
pip install -e .

Now you can import your package from anywhere:

python
from my_project.package_a import module_a1

How Python Actually Handles Imports#

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:

  1. Searches for my_module.py in sys.path

  2. Creates a module object (instance of types.ModuleType)

  3. Executes the module’s code in that object’s namespace

  4. Caches the module object in sys.modules

python
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:

  1. Finds package_a/__init__.py

  2. Creates a module object for package_a

  3. Executes __init__.py in that namespace

  4. Stores it in sys.modules['package_a']

So when you write:

python
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

python
# 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

Real-World Project Example#

Let’s build a practical project structure:

text
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:

python
# 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:

python
# Expose models at package level
from .user import User
from .post import Post

__all__ = ['User', 'Post']

models/post.py:

python
# 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:

python
# 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

Troubleshooting Common Issues#

“No module named X”#

Problem: Python can’t find your module

Solutions:

  1. Check you’re running from the right directory

  2. Verify __init__.py exists in package directories

  3. Check module spelling and path

  4. 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:

  1. Restructure to break the cycle

  2. Move imports inside functions

  3. Use forward references (type hints with quotes)

Wrapping Up#

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__.py to define packages

  • Modules 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

0

Leave a comment