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 (Winter Semester 2025/26). For questions or feedback, reach out via the course forum or office hours.

© 2026 Dominik Mueller   •  Powered by Soopr   •  Theme  Moonwalk