Home

Appendix 2: Understanding Python Modules, Packages, and Imports

appendix python modules packages imports uv

Python Modules

1. Introduction: Demystifying Python’s Import System

Welcome to Appendix 2! This lecture addresses one of the most common sources of confusion for Python developers: the import system. If you’ve ever encountered errors like:

ImportError: attempted relative import with no known parent package
ModuleNotFoundError: No module named 'road_profile_viewer'
ImportError: cannot import name 'find_intersection' from 'geometry'

…then this lecture is for you.

1.1 Why This Matters Now

You’re currently working on the road-profile-viewer refactoring assignment, where you’re transforming a monolithic Python script into a modular project with multiple files:

road_profile_viewer/
├── geometry.py
├── road.py
├── visualization.py
└── main.py

As soon as you split code across multiple files, you need to import functions from one module into another. And that’s where the questions arise:

1.2 What You’ll Learn

By the end of this lecture, you’ll understand:

  1. ✅ What Python modules and packages are (and how they’re different)
  2. ✅ When __init__.py is needed (and when it’s optional)
  3. ✅ Absolute imports: The recommended approach for most projects
  4. ✅ Relative imports: How the dot notation works (and when to use it)
  5. ✅ How external packages like numpy work differently from your own modules
  6. ✅ Common import errors and how to fix them
  7. ✅ Best practices for structuring Python projects

1.3 Prerequisites

This lecture builds on concepts from:

You should have completed the initial setup of the road-profile-viewer project.


2. What is a Python Module?

Let’s start with the foundation: What is a module?

2.1 The Simple Definition

For practical purposes, think of a Python module as a .py file containing Python code.

That’s the simplest mental model. When you create geometry.py, you’ve created a module named geometry.

Official Definition:

According to the Python Glossary, a module is “an object that serves as an organizational unit of Python code.” Technically, modules can come from various sources (.py files, C extensions, built-in modules), but for this course, we focus on .py files—the most common type you’ll work with.

For a comprehensive understanding, see:

2.2 Your Project’s Modules

In your road-profile-viewer project, you have four modules:

road_profile_viewer/
├── geometry.py       ← Module: geometry
├── road.py           ← Module: road
├── visualization.py  ← Module: visualization
└── main.py          ← Module: main

Each file is a self-contained unit of Python code that:

2.3 What’s Inside a Module?

Let’s look at a simplified version of your geometry.py:

# geometry.py
import numpy as np
from typing import Optional, Tuple

def calculate_ray_line(
    x: float,
    ray_angle: float,
    line_start: Tuple[float, float],
    line_end: Tuple[float, float]
) -> Optional[Tuple[float, float]]:
    """Calculate intersection between a ray and a line segment."""
    # Implementation here...
    return intersection_point

def find_intersection(x: float, ray_angle: float, road_profile: list) -> Optional[float]:
    """Find the first intersection of a ray with the road profile."""
    # Implementation here...
    return distance

This module provides two functions that other parts of your code can import and use.

2.4 Modules vs Scripts

Here’s an important distinction:

A Python file can be used in two ways:

  1. As a module (imported by other code):
    from geometry import calculate_ray_line
    
  2. As a script (run directly):
    python geometry.py
    

Most files in your project are modules meant to be imported, not run directly. That’s why you see this pattern:

# main.py
if __name__ == "__main__":
    # This only runs when executed as a script
    main()

This allows main.py to work both ways:

2.5 How Python Finds Modules

When you write import geometry, Python searches for geometry.py in these locations in order:

  1. Current directory (where you run Python from)
  2. Directories in PYTHONPATH (environment variable)
  3. Virtual environment’s site-packages/ (installed packages)
  4. Standard library (built-in modules like os, sys, json)

You can see this list with:

import sys
print(sys.path)

Output (example):

[
  '/home/student/road-profile-viewer',           # Current directory
  '/home/student/.venv/lib/python3.12/site-packages',  # venv packages
  '/usr/lib/python3.12',                         # Standard library
]

This is why import numpy works: numpy is installed in your virtual environment’s site-packages/ directory.

2.6 The Problem with Direct Execution

Try this experiment:

# This will FAIL with import errors:
cd road_profile_viewer
python geometry.py

Error:

ModuleNotFoundError: No module named 'road_profile_viewer'

Why? Because when you run python geometry.py, Python doesn’t know road_profile_viewer is a package. It only sees geometry.py as a standalone script.

The solution (we’ll explain why later):

# This WORKS:
cd ..  # Go to project root
python -m road_profile_viewer.geometry

The -m flag tells Python to treat road_profile_viewer.geometry as a module within a package, not just a script.

2.7 Key Takeaways: Modules


3. What is a Python Package?

Now let’s level up: What’s a package?

3.1 The Simple Definition

A Python package is a directory containing Python modules.

In other words: A package is a folder of .py files that Python treats as a cohesive unit.

3.2 Your Project is a Package

Your road_profile_viewer directory is a Python package:

road_profile_viewer/          ← This is a PACKAGE
├── __init__.py              ← Optional (more on this soon)
├── geometry.py              ← Module inside the package
├── road.py                  ← Module inside the package
├── visualization.py         ← Module inside the package
└── main.py                  ← Module inside the package

This package contains four modules: geometry, road, visualization, and main.

3.3 Packages Enable Hierarchical Organization

Packages let you organize code into logical groups:

my_project/
├── data/
│   ├── __init__.py
│   ├── loader.py
│   └── processor.py
├── models/
│   ├── __init__.py
│   ├── neural_net.py
│   └── decision_tree.py
└── utils/
    ├── __init__.py
    ├── math.py
    └── plotting.py

Here we have three packages (data, models, utils), each containing related modules.

3.4 The Role of __init__.py

This is one of the most common sources of confusion!

3.4.1 Python 3.3+ (Modern Python)

In Python 3.3 and later, __init__.py is optional for basic packages. Python supports “namespace packages” that don’t require __init__.py.

Your project works without __init__.py:

road_profile_viewer/
├── geometry.py
├── road.py
├── visualization.py
└── main.py
# No __init__.py needed!

You can still import modules:

from road_profile_viewer.geometry import find_intersection  # ✅ Works!

3.4.2 When __init__.py is Still Useful

Even though it’s optional, __init__.py serves important purposes:

1. Package Initialization Code

You can run setup code when the package is first imported:

# road_profile_viewer/__init__.py
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

logger.info("Road Profile Viewer package loaded")

⚠️ Important Warning: Minimize Code in __init__.py

While it’s tempting to add initialization code here, keep __init__.py as minimal as possible:

Why? Code in __init__.py runs every time the package is imported, which:

Best Practice: Only add code to __init__.py if it’s absolutely necessary for the package to function. And even then, you probably don’t need it as much as you think! 😉

Better approach for logging:

# Instead of configuring logging in __init__.py, do it in your main entry point:
# main.py
import logging

def main():
    logging.basicConfig(level=logging.INFO)
    logger = logging.getLogger(__name__)
    # Your application code...

2. Convenient Imports

Expose commonly-used functions at the package level:

# road_profile_viewer/__init__.py
from road_profile_viewer.geometry import find_intersection
from road_profile_viewer.road import generate_road_profile
from road_profile_viewer.visualization import create_dash_app

# Now users can do this:
from road_profile_viewer import find_intersection  # ✅ Shorter!

# Instead of:
from road_profile_viewer.geometry import find_intersection  # ✅ Also works

3. Defining __all__

Control what from package import * imports:

# road_profile_viewer/__init__.py
__all__ = ['find_intersection', 'generate_road_profile', 'create_dash_app']

⚠️ Important Notes About __all__ and import *

While __all__ is useful for library authors to define a public API, there are important caveats:

About import * (star imports):

Why __all__ still matters:

Best Practice:

# ❌ DON'T DO THIS:
from road_profile_viewer import *  # What did this import? Who knows!

# ✅ DO THIS INSTEAD:
from road_profile_viewer import find_intersection, generate_road_profile  # Explicit and clear

Performance Note: In large projects, even defining __all__ can have implications. If your __init__.py imports many modules just to populate __all__, you’re loading code that might not be needed. This is why many large libraries use lazy imports or keep __all__ minimal.

4. Package Metadata

Store version information and package-level constants:

# road_profile_viewer/__init__.py
__version__ = "1.0.0"
__author__ = "Your Name"

# Usage elsewhere:
import road_profile_viewer
print(road_profile_viewer.__version__)

3.4.3 Regular Packages vs Namespace Packages

Regular Package (has __init__.py):

my_package/
├── __init__.py    ← Makes this a regular package
└── module.py

Namespace Package (no __init__.py):

my_package/
└── module.py      ← Still works in Python 3.3+

When to use each:

3.5 Hands-On: Adding __init__.py to Your Project

Let’s add a minimal __init__.py to your road-profile-viewer:

Create road_profile_viewer/__init__.py:

"""
Road Profile Viewer

A Python package for visualizing road elevation profiles with interactive ray tracing.
"""

__version__ = "1.0.0"

# Convenient imports for common use cases
from road_profile_viewer.geometry import find_intersection
from road_profile_viewer.road import generate_road_profile
from road_profile_viewer.visualization import create_dash_app

__all__ = [
    'find_intersection',
    'generate_road_profile',
    'create_dash_app',
]

Benefits:

# Before (namespace package):
from road_profile_viewer.geometry import find_intersection

# After (regular package with __init__.py):
from road_profile_viewer import find_intersection  # ✅ Shorter!

# Version info:
import road_profile_viewer
print(road_profile_viewer.__version__)  # ✅ "1.0.0"

3.6 Nested Packages

Packages can contain other packages:

road_profile_viewer/
├── __init__.py
├── main.py
├── core/
│   ├── __init__.py
│   ├── geometry.py
│   └── road.py
└── ui/
    ├── __init__.py
    └── visualization.py

Now you have:

Import paths reflect this structure:

from road_profile_viewer.core.geometry import find_intersection
from road_profile_viewer.ui.visualization import create_dash_app

3.7 Key Takeaways: Packages


Now let’s tackle the most important import style: absolute imports.

4.1 What are Absolute Imports?

Absolute imports use the full path from the project root.

Syntax:

from package.module import function

4.2 Your Project’s Absolute Imports

In your road-profile-viewer, absolute imports look like this:

In visualization.py:

# Import from geometry module
from road_profile_viewer.geometry import find_intersection

# Import from road module
from road_profile_viewer.road import generate_road_profile

In main.py:

# Import from visualization module
from road_profile_viewer.visualization import create_dash_app

4.3 How Absolute Imports Work

When Python sees from road_profile_viewer.geometry import find_intersection, it:

  1. Searches for road_profile_viewer in sys.path
  2. Looks for geometry.py inside that package
  3. Imports the find_intersection function from that module

Visual representation:

sys.path includes: /home/student/project/
                   ↓
                   road_profile_viewer/    ← Found!
                   ↓
                   geometry.py             ← Found!
                   ↓
                   def find_intersection   ← Import this!

4.4 Why the Assignment Requires Absolute Imports

Your GitHub Classroom assignment states:

Absolute imports are mandatory to avoid “ImportError: attempted relative import with no known parent package.”

Why this requirement? Let’s see what happens with different run commands:

Scenario 1: Running as a script (FAILS with relative imports)

cd road_profile_viewer
python main.py

If main.py uses relative imports like from .visualization import create_dash_app, you get:

ImportError: attempted relative import with no known parent package

Why? Python doesn’t know road_profile_viewer is a package when you run python main.py directly.

Scenario 2: Running as a module (WORKS)

cd ..  # Project root
python -m road_profile_viewer.main

Now Python knows:

Scenario 3: Using absolute imports (ALWAYS WORKS)

# From anywhere:
python -m road_profile_viewer.main  # ✅ Works
cd road_profile_viewer && python main.py  # ✅ Also works!

Absolute imports work because they don’t depend on Python knowing the package structure - they use sys.path.

4.5 Running Modules: The -m Flag

Best practice: Always use -m to run modules in packages:

# ✅ GOOD: Run as module
python -m road_profile_viewer.main

# ❌ BAD: Run as script (breaks relative imports)
python road_profile_viewer/main.py

The -m flag tells Python:

4.6 How uv run Handles This

When you use uv run:

uv run python -m road_profile_viewer.main

uv automatically:

4.7 Complete Example: Absolute Imports in Your Project

File: road_profile_viewer/geometry.py

"""Geometry calculations for ray-line intersections."""
import numpy as np
from typing import Optional, Tuple

def find_intersection(x: float, ray_angle: float, road_profile: list) -> Optional[float]:
    """Find intersection between a ray and road profile."""
    # Implementation...
    return distance

File: road_profile_viewer/road.py

"""Road profile generation."""
import numpy as np

def generate_road_profile(num_points: int = 100) -> np.ndarray:
    """Generate a random road elevation profile."""
    # Implementation...
    return profile

File: road_profile_viewer/visualization.py

"""Dash web application for visualization."""
import dash
from dash import dcc, html
import plotly.graph_objects as go

# ✅ Absolute imports
from road_profile_viewer.geometry import find_intersection
from road_profile_viewer.road import generate_road_profile

def create_dash_app():
    """Create and return a Dash application."""
    app = dash.Dash(__name__)

    # Use imported functions
    profile = generate_road_profile()
    distance = find_intersection(0, 45, profile)

    # Build UI...
    return app

File: road_profile_viewer/main.py

"""Entry point for the Road Profile Viewer application."""

# ✅ Absolute import
from road_profile_viewer.visualization import create_dash_app

def main():
    """Run the application."""
    app = create_dash_app()
    app.run_server(debug=True, host='127.0.0.1', port=8050)

if __name__ == "__main__":
    main()

Running the app:

# From project root:
uv run python -m road_profile_viewer.main

4.8 Advantages of Absolute Imports

✅ Clarity: Immediately obvious where code comes from ✅ Reliability: Works regardless of how the module is run ✅ IDE Support: Better autocomplete and navigation ✅ Refactoring Safety: Moving files doesn’t break imports (as long as package structure stays the same) ✅ No Ambiguity: Can’t be confused with standard library or external packages

4.9 Key Takeaways: Absolute Imports


5. Relative Imports: When and How

Now let’s explore relative imports - the alternative approach using dots.

5.1 What are Relative Imports?

Relative imports use dots to navigate the package hierarchy.

Syntax:

from .module import function       # Current package
from ..module import function      # Parent package
from ...module import function     # Grandparent package

5.2 The Dot Notation Explained

Think of dots like filesystem paths:

5.3 Your Project with Relative Imports

File: road_profile_viewer/visualization.py

# Relative imports (alternative to absolute)
from .geometry import find_intersection      # ← Single dot
from .road import generate_road_profile      # ← Single dot

Translation:

File: road_profile_viewer/main.py

# Relative import
from .visualization import create_dash_app   # ← Single dot

5.4 Understanding the Dots: Visual Guide

Imagine this more complex structure:

my_project/
├── package_a/
│   ├── __init__.py
│   ├── module1.py
│   └── sub_package/
│       ├── __init__.py
│       └── module2.py
└── package_b/
    ├── __init__.py
    └── module3.py

From package_a/sub_package/module2.py:

# Import from same package (sub_package)
from . import module2           # ← Current package (sub_package)

# Import from parent package (package_a)
from .. import module1          # ← One level up

# Import from sibling module in parent package
from ..module1 import function  # ← Up then into module1

# Import from completely different package (DOESN'T WORK)
from ...package_b import module3  # ❌ Can't go above root!

Visual representation:

module2.py location: package_a/sub_package/module2.py

from .       → package_a/sub_package/  (current)
from ..      → package_a/               (one up)
from ...     → my_project/              (two up - root)
from ....    → ❌ ERROR: beyond root

5.5 Relative vs Absolute: Side-by-Side

Let’s compare the same imports written both ways:

Absolute imports:

# visualization.py
from road_profile_viewer.geometry import find_intersection
from road_profile_viewer.road import generate_road_profile

Relative imports:

# visualization.py
from .geometry import find_intersection
from .road import generate_road_profile

Both work the same way! The choice is a matter of style and project requirements.

5.6 When Relative Imports Break: The Infamous Error

Try running your code with relative imports:

cd road_profile_viewer
python main.py

Error:

ImportError: attempted relative import with no known parent package

Why does this happen?

When you run python main.py, Python treats main.py as a standalone script, not as a module in a package. Relative imports require Python to know:

Running directly as a script, Python doesn’t have this information!

The fix:

# Run as a module, not a script:
cd ..  # Go to project root
python -m road_profile_viewer.main  # ✅ Works!

Now Python knows:

5.7 When to Use Relative Imports

Relative imports are useful when:

✅ Large packages with deep hierarchies ✅ Package will be renamed (relative imports don’t hardcode the package name) ✅ Sub-packages that should be self-contained

Example: A library that might be forked:

# If your package might be renamed from "original_name" to "forked_name"
# Relative imports don't break:
from .utils import helper  # ✅ Works with any package name

# Absolute imports would need updates:
from original_name.utils import helper  # ❌ Breaks after renaming

5.8 When to Use Absolute Imports

Absolute imports are better when:

✅ Small to medium projects (like road-profile-viewer) ✅ Running modules as scripts (no parent package confusion) ✅ Team projects (clearer, more explicit) ✅ Course assignments (required for this class!)

5.9 Mixing Absolute and Relative

You can mix both styles in the same project:

# visualization.py
from road_profile_viewer.geometry import find_intersection  # Absolute
from .road import generate_road_profile                      # Relative
import numpy as np                                           # External package

However, consistency is better! Pick one style and stick to it throughout your project.

5.10 Key Takeaways: Relative Imports


6. Absolute vs Relative: The Practical Comparison

Let’s do a comprehensive comparison to help you make informed decisions.

6.1 Scenario: Refactoring to Subdirectories

Imagine you’re reorganizing your project into subdirectories:

Current structure:

road_profile_viewer/
├── geometry.py
├── road.py
├── visualization.py
└── main.py

New structure:

road_profile_viewer/
├── core/
│   ├── geometry.py
│   └── road.py
├── ui/
│   └── visualization.py
└── main.py

6.2 With Absolute Imports

Before (flat structure):

# visualization.py
from road_profile_viewer.geometry import find_intersection
from road_profile_viewer.road import generate_road_profile

After (nested structure) - REQUIRES CHANGES:

# ui/visualization.py
from road_profile_viewer.core.geometry import find_intersection
from road_profile_viewer.core.road import generate_road_profile

Impact: ❌ Must update import paths in all files

6.3 With Relative Imports

Before (flat structure):

# visualization.py
from .geometry import find_intersection
from .road import generate_road_profile

After (nested structure) - REQUIRES CHANGES TOO:

# ui/visualization.py
from ..core.geometry import find_intersection
from ..core.road import generate_road_profile

Impact: ❌ Must update import paths (but only dots change, not the full path)

6.4 Trade-offs Table

Aspect Absolute Imports Relative Imports
Clarity ✅ Immediately clear where code comes from ⚠️ Must mentally navigate dots
Reliability ✅ Works as script or module ❌ Only works with python -m
IDE Support ✅ Better autocomplete and navigation ⚠️ May struggle with complex nesting
Refactoring ❌ Must update full paths when restructuring ⚠️ Must update dots when changing levels
Package Renaming ❌ Must update all imports if package is renamed ✅ No changes needed
Learning Curve ✅ Easier for beginners ⚠️ Requires understanding package hierarchy
Verbosity ⚠️ Longer import statements ✅ Shorter, more concise

6.5 Real Code Comparison

Let’s see a complete example side-by-side:

File structure:

road_profile_viewer/
├── core/
│   ├── __init__.py
│   ├── geometry.py
│   └── road.py
├── ui/
│   ├── __init__.py
│   └── visualization.py
└── main.py

Absolute imports version:

# ui/visualization.py
from road_profile_viewer.core.geometry import find_intersection
from road_profile_viewer.core.road import generate_road_profile
import dash

def create_dash_app():
    profile = generate_road_profile()
    return dash.Dash(__name__)
# main.py
from road_profile_viewer.ui.visualization import create_dash_app

def main():
    app = create_dash_app()
    app.run_server()

if __name__ == "__main__":
    main()

Relative imports version:

# ui/visualization.py
from ..core.geometry import find_intersection
from ..core.road import generate_road_profile
import dash

def create_dash_app():
    profile = generate_road_profile()
    return dash.Dash(__name__)
# main.py
from .ui.visualization import create_dash_app

def main():
    app = create_dash_app()
    app.run_server()

if __name__ == "__main__":
    main()

Running them:

# Absolute imports: Works both ways
python road_profile_viewer/main.py              # ✅ Works
python -m road_profile_viewer.main              # ✅ Works

# Relative imports: Only works one way
python road_profile_viewer/main.py              # ❌ ImportError!
python -m road_profile_viewer.main              # ✅ Works

6.6 Recommendation for This Course

Use absolute imports for the road-profile-viewer assignment because:

  1. ✅ Required by the assignment specification
  2. ✅ More reliable for beginners
  3. ✅ Better IDE support in VS Code
  4. ✅ Easier for peer code review
  5. ✅ Consistent with course examples

Format:

from road_profile_viewer.geometry import find_intersection
from road_profile_viewer.road import generate_road_profile
from road_profile_viewer.visualization import create_dash_app

6.7 Key Takeaways: Comparison


7. External Packages: How import numpy Just Works

Now let’s answer the burning question: Why can I write import numpy as np but I can’t do that with my own modules?

7.1 The Mystery of Simple Imports

You’ve noticed this pattern:

# External packages: Simple imports ✅
import numpy as np
import pandas as pd
import dash

# Your own code: Full path required ❌
import geometry  # ModuleNotFoundError!

# Your own code: Must use full path ✅
from road_profile_viewer.geometry import find_intersection

Why the difference?

7.2 Where External Packages Live

When you run uv add numpy, uv installs numpy into your virtual environment’s site-packages/ directory:

.venv/
└── lib/
    └── python3.12/
        └── site-packages/
            ├── numpy/              ← numpy package
            │   ├── __init__.py
            │   ├── core/
            │   ├── linalg/
            │   └── ...
            ├── pandas/             ← pandas package
            ├── dash/               ← dash package
            └── plotly/             ← plotly package

7.3 How Python Finds External Packages

Remember sys.path? It includes site-packages/:

import sys
print(sys.path)

Output:

[
  '/home/student/road-profile-viewer',                        # Current directory
  '/home/student/.venv/lib/python3.12/site-packages',         # ← Installed packages!
  '/usr/lib/python3.12',                                       # Standard library
]

When you write import numpy, Python:

  1. Searches sys.path in order
  2. Finds numpy/ directory in site-packages/
  3. Imports numpy/__init__.py
  4. Success!

7.4 Why Your Own Modules Don’t Work This Way

Your project structure:

road-profile-viewer/              ← Project root (in sys.path)
└── road_profile_viewer/          ← Package directory
    ├── geometry.py               ← Module
    ├── road.py                   ← Module
    └── main.py                   ← Module

When you write import geometry, Python:

  1. Searches sys.path
  2. Looks for geometry.py or geometry/ directory in project root
  3. Doesn’t find it (because it’s inside road_profile_viewer/)
  4. ModuleNotFoundError!

Your modules are nested inside a package, so you must use the full path:

from road_profile_viewer.geometry import find_intersection  # ✅ Works

7.5 Python’s Import Search Order

When you import something, Python searches in this order:

  1. Built-in modules (like sys, os, json)
  2. Current directory (where you run Python from)
  3. Directories in PYTHONPATH (environment variable)
  4. Virtual environment’s site-packages/ (installed packages)
  5. System Python’s site-packages/ (global packages - usually ignored in venv)
  6. Standard library (Python’s built-in modules)

Example resolution:

import sys          # ← Found in built-in modules (step 1)
import numpy        # ← Found in venv's site-packages (step 4)
import geometry     # ← Not found anywhere → ModuleNotFoundError!

7.6 Installing Your Own Package (Advanced)

What if you want to import your own code like import road_profile_viewer?

You can install your own project as a package into site-packages/:

# Install your project in "editable" mode
uv pip install -e .

This creates a link in site-packages/ pointing to your project:

.venv/lib/python3.12/site-packages/
├── numpy/
├── dash/
└── road_profile_viewer.egg-link  ← Points to your project!

Now this works:

# From anywhere, not just project root:
import road_profile_viewer
from road_profile_viewer.geometry import find_intersection

When to do this:

When NOT to do this (for this course):

7.7 The Danger: Naming Conflicts

⚠️ NEVER name your modules the same as standard library or popular packages!

Bad idea:

my_project/
├── numpy.py     ← ❌ Shadows the real numpy!
├── json.py      ← ❌ Shadows built-in json!
└── main.py

What happens:

# main.py
import numpy  # ← Imports YOUR numpy.py, not the real numpy package!

Python searches the current directory first, so your numpy.py is found before the real numpy in site-packages/.

Result: Nothing works, and error messages are confusing!

How to avoid:

✅ Use descriptive, unique names for your modules ✅ Never use names from the standard library (os, sys, json, math, etc.) ✅ Check PyPI before naming a package: pypi.org

7.8 Visualizing the Difference

External packages:

import numpy
       ↓
sys.path includes .venv/lib/python3.12/site-packages/
       ↓
Found: site-packages/numpy/
       ↓
Import numpy/__init__.py
       ↓
✅ Success!

Your modules:

import geometry
       ↓
sys.path includes /home/student/road-profile-viewer/
       ↓
NOT found: /home/student/road-profile-viewer/geometry.py
             (because it's in road_profile_viewer/ subdirectory)
       ↓
❌ ModuleNotFoundError!

CORRECT WAY:
from road_profile_viewer.geometry import ...
       ↓
sys.path includes /home/student/road-profile-viewer/
       ↓
Found: /home/student/road-profile-viewer/road_profile_viewer/
       ↓
Found: geometry.py inside road_profile_viewer/
       ↓
✅ Success!

7.9 Key Takeaways: External Packages


8. Hands-On: Exploring Your Project’s Import System

Time to get your hands dirty! Let’s explore how imports work in your actual project.

8.1 Exercise 1: Inspect sys.path

Create a new file: road_profile_viewer/debug_imports.py

"""Debug script to understand Python's import system."""
import sys
import os

print("=" * 60)
print("PYTHON IMPORT SYSTEM DEBUG")
print("=" * 60)

print("\n1. CURRENT WORKING DIRECTORY:")
print(f"   {os.getcwd()}")

print("\n2. SCRIPT LOCATION:")
print(f"   {__file__}")

print("\n3. sys.path (where Python searches for modules):")
for i, path in enumerate(sys.path, 1):
    print(f"   [{i}] {path}")

print("\n4. VIRTUAL ENVIRONMENT:")
venv = os.environ.get('VIRTUAL_ENV', 'Not activated')
print(f"   {venv}")

print("\n5. TRYING TO IMPORT OUR MODULES:")
try:
    from road_profile_viewer.geometry import find_intersection
    print("   ✅ Successfully imported from road_profile_viewer.geometry")
except ImportError as e:
    print(f"   ❌ Failed to import: {e}")

print("\n6. TRYING TO IMPORT EXTERNAL PACKAGES:")
try:
    import numpy
    print(f"   ✅ numpy version {numpy.__version__} from {numpy.__file__}")
except ImportError as e:
    print(f"   ❌ Failed to import numpy: {e}")

print("=" * 60)

Run it:

# Make sure you're in project root
cd /path/to/road-profile-viewer

# Run as a module
uv run python -m road_profile_viewer.debug_imports

Expected output:

============================================================
PYTHON IMPORT SYSTEM DEBUG
============================================================

1. CURRENT WORKING DIRECTORY:
   /home/student/road-profile-viewer

2. SCRIPT LOCATION:
   /home/student/road-profile-viewer/road_profile_viewer/debug_imports.py

3. sys.path (where Python searches for modules):
   [1] /home/student/road-profile-viewer
   [2] /home/student/road-profile-viewer/.venv/lib/python3.12/site-packages
   [3] /usr/lib/python3.12
   [4] /usr/lib/python3.12/lib-dynload

4. VIRTUAL ENVIRONMENT:
   /home/student/road-profile-viewer/.venv

5. TRYING TO IMPORT OUR MODULES:
   ✅ Successfully imported from road_profile_viewer.geometry

6. TRYING TO IMPORT EXTERNAL PACKAGES:
   ✅ numpy version 1.26.0 from /home/student/road-profile-viewer/.venv/lib/python3.12/site-packages/numpy/__init__.py
============================================================

Observations:

8.2 Exercise 2: Add Convenient Imports with __init__.py

Create/edit: road_profile_viewer/__init__.py

"""
Road Profile Viewer

A Python package for visualizing road elevation profiles.
"""

__version__ = "1.0.0"
__author__ = "Your Name"

# Convenient imports - expose commonly used functions at package level
from road_profile_viewer.geometry import find_intersection
from road_profile_viewer.road import generate_road_profile
from road_profile_viewer.visualization import create_dash_app

__all__ = [
    'find_intersection',
    'generate_road_profile',
    'create_dash_app',
]

# Optional: Log when package is imported
import logging
logger = logging.getLogger(__name__)
logger.info(f"Road Profile Viewer v{__version__} loaded")

Test it:

uv run python
>>> import road_profile_viewer
>>> print(road_profile_viewer.__version__)
1.0.0

>>> # Now we can import from the package level!
>>> from road_profile_viewer import find_intersection
>>> print(find_intersection)
<function find_intersection at 0x...>

>>> # Instead of the longer:
>>> from road_profile_viewer.geometry import find_intersection

Benefits:

8.3 Exercise 3: Compare Absolute vs Relative Imports

Create a test file: road_profile_viewer/import_test.py

"""Test different import styles."""

print("Testing Absolute Imports:")
try:
    from road_profile_viewer.geometry import find_intersection
    print("✅ Absolute import works!")
except ImportError as e:
    print(f"❌ Absolute import failed: {e}")

print("\nTesting Relative Imports:")
try:
    from .geometry import find_intersection
    print("✅ Relative import works!")
except ImportError as e:
    print(f"❌ Relative import failed: {e}")

print("\nTesting Simple Import (will fail):")
try:
    from geometry import find_intersection
    print("✅ Simple import works!")
except ImportError as e:
    print(f"❌ Simple import failed: {e}")

Test as module:

uv run python -m road_profile_viewer.import_test

Expected output:

Testing Absolute Imports:
✅ Absolute import works!

Testing Relative Imports:
✅ Relative import works!

Testing Simple Import (will fail):
❌ Simple import failed: No module named 'geometry'

Now try running as a script (will fail):

cd road_profile_viewer
uv run python import_test.py

Expected output:

Testing Absolute Imports:
✅ Absolute import works!

Testing Relative Imports:
❌ Relative import failed: attempted relative import with no known parent package

Testing Simple Import (will fail):
❌ Simple import failed: No module named 'geometry'

Key lesson: Relative imports only work when run with python -m!

8.4 Exercise 4: Explore Where numpy Lives

uv run python
>>> import numpy
>>> print(numpy.__file__)
/home/student/road-profile-viewer/.venv/lib/python3.12/site-packages/numpy/__init__.py

>>> # Better: Use __path__ to see the package directory directly
>>> print(numpy.__path__)
['/home/student/road-profile-viewer/.venv/lib/python3.12/site-packages/numpy']

>>> # See the entire package directory
>>> import os
>>> numpy_dir = os.path.dirname(numpy.__file__)
>>> print(f"numpy is installed at: {numpy_dir}")
numpy is installed at: /home/student/road-profile-viewer/.venv/lib/python3.12/site-packages/numpy

>>> # List some of numpy's modules
>>> os.listdir(numpy_dir)[:10]
['__init__.py', '__pycache__', 'core', 'distutils', 'doc', 'f2py', 'fft', 'lib', 'linalg', 'ma']

Observations:

8.5 Exercise 5: See What’s Importable from Your Package

>>> import road_profile_viewer
>>> dir(road_profile_viewer)
['__all__', '__author__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', 'create_dash_app', 'find_intersection', 'generate_road_profile', 'logging', 'logger']

>>> # Only exported names (from __all__):
>>> road_profile_viewer.__all__
['find_intersection', 'generate_road_profile', 'create_dash_app']

This shows what your __init__.py exposed!

8.6 Key Takeaways: Hands-On Exploration


9. Common Import Errors and Solutions

Let’s tackle the most common import errors you’ll encounter and how to fix them.

9.1 Error 1: ImportError: attempted relative import with no known parent package

When you see it:

$ python road_profile_viewer/main.py
ImportError: attempted relative import with no known parent package

What happened:

You’re using relative imports (like from .visualization import) but running the file as a script instead of as a module.

Why it happens:

When you run python road_profile_viewer/main.py, Python doesn’t know that main.py is part of the road_profile_viewer package. Relative imports require package context.

Solutions:

Option 1: Use python -m (recommended)

cd /path/to/project-root
python -m road_profile_viewer.main

Option 2: Use absolute imports instead

# Change this:
from .visualization import create_dash_app

# To this:
from road_profile_viewer.visualization import create_dash_app

Option 3: Use uv run

uv run python -m road_profile_viewer.main

9.2 Error 2: ModuleNotFoundError: No module named 'road_profile_viewer'

When you see it:

$ python -m road_profile_viewer.main
ModuleNotFoundError: No module named 'road_profile_viewer'

What happened:

Python can’t find your road_profile_viewer package in sys.path.

Why it happens:

Solutions:

Option 1: Run from correct directory

# Make sure you're in project root:
cd /path/to/road-profile-viewer  # The directory containing road_profile_viewer/
python -m road_profile_viewer.main

Option 2: Check your project structure

ls
# Should see:
# road_profile_viewer/  ← This is the package
# pyproject.toml
# README.md
# etc.

Option 3: Use uv run (handles this automatically)

uv run python -m road_profile_viewer.main

9.3 Error 3: ImportError: cannot import name 'find_intersection' from 'geometry'

When you see it:

from geometry import find_intersection
ImportError: cannot import name 'find_intersection' from 'geometry'

What happened:

Python found a geometry module, but it doesn’t have find_intersection.

Why it happens:

Solutions:

Option 1: Use full import path

# Instead of:
from geometry import find_intersection

# Use:
from road_profile_viewer.geometry import find_intersection

Option 2: Check for naming conflicts

# Make sure you don't have multiple geometry.py files:
find . -name "geometry.py"

Option 3: Verify the function exists

# Check what's actually in the module:
from road_profile_viewer import geometry
print(dir(geometry))

9.4 Error 4: ModuleNotFoundError: No module named 'numpy' (or other external package)

When you see it:

import numpy as np
ModuleNotFoundError: No module named 'numpy'

What happened:

numpy isn’t installed in your virtual environment.

Why it happens:

Solutions:

Option 1: Sync dependencies with uv

uv sync

Option 2: Manually install the package

uv add numpy

Option 3: Check which Python you’re using

which python  # Mac/Linux
where python  # Windows

# Should point to .venv/bin/python or .venv\Scripts\python.exe

Option 4: Always use uv run

uv run python -m road_profile_viewer.main

9.5 Error 5: Circular Import

When you see it:

ImportError: cannot import name 'function_a' from partially initialized module 'module_b'
(most likely due to a circular import)

What happened:

Two modules import from each other, creating a dependency loop.

Example:

# geometry.py
from road_profile_viewer.road import some_function

# road.py
from road_profile_viewer.geometry import find_intersection

Python can’t initialize either module because they both need the other to be initialized first!

Solutions:

Option 1: Restructure code (best)

Move shared dependencies to a separate module:

# utils.py
def shared_function():
    pass

# geometry.py
from road_profile_viewer.utils import shared_function

# road.py
from road_profile_viewer.utils import shared_function

Option 2: Move import inside function

# geometry.py
def calculate_something():
    # Import here instead of at the top
    from road_profile_viewer.road import some_function
    return some_function()

Option 3: Use type hints with TYPE_CHECKING

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    # Only imported during type checking, not at runtime
    from road_profile_viewer.road import RoadProfile

9.6 Error 6: NameError: name 'find_intersection' is not defined

When you see it:

distance = find_intersection(x, angle, profile)
NameError: name 'find_intersection' is not defined

What happened:

You forgot to import the function!

Solution:

# Add the import at the top of the file:
from road_profile_viewer.geometry import find_intersection

# Then use it:
distance = find_intersection(x, angle, profile)

9.7 Debugging Checklist

When you hit import errors, check:

  1. ✅ Are you in the correct directory? (project root)
  2. ✅ Are you using python -m package.module instead of python file.py?
  3. ✅ Is your virtual environment activated? (or using uv run?)
  4. ✅ Are dependencies installed? (run uv sync)
  5. ✅ Is the import path correct? (check spelling, package structure)
  6. ✅ Are you using absolute imports? (recommended for this course)
  7. ✅ Does the function/class actually exist in the module?

9.8 Key Takeaways: Import Errors


10. Best Practices Summary

Let’s wrap up with actionable best practices for your Python projects.

10.1 Import Style Guidelines

1. Use Absolute Imports (Course Requirement)

# ✅ GOOD: Absolute imports
from road_profile_viewer.geometry import find_intersection
from road_profile_viewer.road import generate_road_profile
from road_profile_viewer.visualization import create_dash_app

# ❌ AVOID: Relative imports (for this course)
from .geometry import find_intersection
from .road import generate_road_profile
from .visualization import create_dash_app

2. Group Imports Properly

# Standard library imports
import os
import sys
from pathlib import Path

# External package imports
import numpy as np
import pandas as pd
import dash
from dash import dcc, html

# Local package imports
from road_profile_viewer.geometry import find_intersection
from road_profile_viewer.road import generate_road_profile

3. Use Specific Imports

# ✅ GOOD: Import what you need
from road_profile_viewer.geometry import find_intersection, calculate_ray_line

# ⚠️ OK but less clear:
import road_profile_viewer.geometry
# Then use: road_profile_viewer.geometry.find_intersection()

# ❌ AVOID: Star imports
from road_profile_viewer.geometry import *  # Who knows what this imports?

10.2 Project Structure Best Practices

1. Keep __init__.py Minimal

# road_profile_viewer/__init__.py
"""Road Profile Viewer package."""

__version__ = "1.0.0"

# Only expose main API
from road_profile_viewer.visualization import create_dash_app

__all__ = ['create_dash_app']

2. Organize by Functionality

road_profile_viewer/
├── __init__.py
├── main.py              # Entry point
├── core/                # Core logic
│   ├── __init__.py
│   ├── geometry.py
│   └── road.py
├── ui/                  # User interface
│   ├── __init__.py
│   └── visualization.py
└── utils/               # Utilities
    ├── __init__.py
    └── helpers.py

3. Never Shadow Standard Library Names

# ❌ BAD: Naming conflicts
json.py       # Shadows built-in json
math.py       # Shadows built-in math
sys.py        # Shadows built-in sys
os.py         # Shadows built-in os

# ✅ GOOD: Descriptive, unique names
json_parser.py
math_utils.py
system_info.py
os_helpers.py

10.3 Running Code Best Practices

1. Always Use -m for Package Modules

# ✅ GOOD: Run as module
python -m road_profile_viewer.main

# ❌ BAD: Run as script (breaks relative imports)
python road_profile_viewer/main.py

2. Use uv run for Convenience

# ✅ BEST: Let uv handle everything
uv run python -m road_profile_viewer.main

# ✅ ALSO GOOD: Run scripts defined in pyproject.toml
uv run road-viewer

3. Run from Project Root

# Always run commands from the directory containing your package:
/path/to/road-profile-viewer/  ← Run from here
├── road_profile_viewer/       ← Your package
├── pyproject.toml
└── README.md

10.4 Dependency Management Best Practices

1. Use pyproject.toml as Single Source of Truth

[project]
name = "road-profile-viewer"
version = "1.0.0"
dependencies = [
    "dash>=2.14.0",
    "plotly>=5.18.0",
    "pandas>=2.1.0",
    "numpy>=1.26.0",
]

2. Separate Dev Dependencies

[project.optional-dependencies]
dev = [
    "pytest>=7.4.0",
    "ruff>=0.1.0",
    "pyright>=1.1.0",
]

3. Use Version Constraints Wisely

dependencies = [
    "numpy>=1.26.0",        # ✅ Allow patch/minor updates
    "dash>=2.14,<3.0",      # ✅ Lock major version
    "pandas==2.1.0",        # ⚠️ Too strict, avoid unless necessary
]

10.5 Testing Your Imports

Create a test script to verify everything works:

# test_imports.py
"""Verify all imports work correctly."""

def test_imports():
    """Test that all main imports work."""
    from road_profile_viewer.geometry import find_intersection
    from road_profile_viewer.road import generate_road_profile
    from road_profile_viewer.visualization import create_dash_app

    print("✅ All imports successful!")

    # Verify functions are callable
    assert callable(find_intersection)
    assert callable(generate_road_profile)
    assert callable(create_dash_app)

    print("✅ All functions are callable!")

if __name__ == "__main__":
    test_imports()

Run it:

uv run python -m test_imports

10.6 Documentation Best Practices

1. Document Module Purpose

# geometry.py
"""
Geometric calculations for ray-line intersections.

This module provides functions for calculating intersections between
rays and road profile line segments.
"""

2. Document Import Dependencies

# visualization.py
"""
Interactive Dash web application for road profile visualization.

Dependencies:
    - dash: Web application framework
    - plotly: Interactive plotting
    - road_profile_viewer.geometry: Ray intersection calculations
    - road_profile_viewer.road: Road profile generation
"""

10.7 Quick Reference: Import Patterns

Your own modules:

# ✅ Use absolute imports
from road_profile_viewer.geometry import find_intersection
from road_profile_viewer.road import generate_road_profile

External packages:

# ✅ Simple imports
import numpy as np
import pandas as pd

Standard library:

# ✅ Simple imports
import os
import sys
from pathlib import Path

Running your code:

# ✅ As a module (from project root)
uv run python -m road_profile_viewer.main

# ✅ Using scripts defined in pyproject.toml
uv run road-viewer

10.8 Final Checklist for the Assignment

Before submitting your refactored road-profile-viewer:

10.9 Key Takeaways: Best Practices


11. Conclusion: You Now Understand Python’s Import System

Congratulations! You’ve completed a comprehensive journey through Python’s import system. Let’s recap what you’ve learned:

11.1 What You Now Know

  1. ✅ Modules: Every .py file is a module with its own namespace
  2. ✅ Packages: Directories containing modules, organized hierarchically
  3. ✅ __init__.py: Optional in Python 3.3+ but useful for initialization and convenient imports
  4. ✅ Absolute Imports: Use full paths from package root (from road_profile_viewer.geometry import ...)
  5. ✅ Relative Imports: Use dots to navigate package hierarchy (. = current, .. = parent)
  6. ✅ External Packages: Installed in site-packages/, importable with simple names (import numpy)
  7. ✅ sys.path: Python’s search path determines where modules are found
  8. ✅ Common Errors: How to diagnose and fix import issues
  9. ✅ Best Practices: Guidelines for professional Python projects

11.2 Why This Matters for Your Career

Understanding Python’s import system is fundamental because:

11.3 How This Applies to Your Assignment

For the road-profile-viewer refactoring assignment:

  1. ✅ Use absolute imports exclusively: from road_profile_viewer.module import function
  2. ✅ Run with python -m road_profile_viewer.main or uv run python -m road_profile_viewer.main
  3. ✅ Keep your package structure flat (no need for subdirectories)
  4. ✅ Optionally add __init__.py for package metadata and convenient imports
  5. ✅ Ensure all imports work by testing with uv sync && uv run python -m road_profile_viewer.main

11.4 Going Further (Optional Reading)

If you want to dive deeper into Python’s import system:

11.5 Resources for the Assignment

11.6 Practice Exercises (Optional)

Want to solidify your understanding? Try these exercises:

Exercise 1: Add More Modules

Create a new module road_profile_viewer/config.py with configuration constants:

# config.py
"""Configuration constants for the application."""

DEFAULT_NUM_POINTS = 100
DEFAULT_RAY_ANGLE = 45
DEFAULT_PORT = 8050

Import and use it in your other modules.

Exercise 2: Create a Utilities Package

Organize helper functions into a sub-package:

road_profile_viewer/
├── utils/
│   ├── __init__.py
│   ├── math_helpers.py
│   └── plotting_helpers.py

Exercise 3: Add Package Metadata

Create a comprehensive __init__.py with version info, convenient imports, and logging setup.

Exercise 4: Test Import Styles

Create branches in your Git repo to experiment:


12. Next Steps

Now that you understand Python’s import system, you’re ready to:

  1. ✅ Complete your road-profile-viewer refactoring with confidence
  2. ✅ Structure larger Python projects professionally
  3. ✅ Debug import errors quickly and effectively
  4. ✅ Collaborate with teams using consistent conventions
  5. ✅ Read and understand open-source Python codebases

Happy coding, and may all your imports resolve successfully! 🐍


This appendix is part of the Software Engineering course at Hochschule Aalen (Summer Semester 2026). For questions or feedback, reach out via the course forum or office hours.

© 2026 Dominik Mueller   •  Powered by Soopr   •  Theme  Moonwalk