Appendix 2: Understanding Python Modules, Packages, and Imports
October 2025 (10437 Words, 58 Minutes)

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:
- What exactly is a “module” vs a “package”?
- Do I need
__init__.pyfiles? - Should I write
from .visualization importorfrom road_profile_viewer.visualization import? - What do those dots mean? What’s the difference between
.,.., and...? - Why can I just write
import numpybut I can’t do that with my own code?
1.2 What You’ll Learn
By the end of this lecture, you’ll understand:
- ✅ What Python modules and packages are (and how they’re different)
- ✅ When
__init__.pyis needed (and when it’s optional) - ✅ Absolute imports: The recommended approach for most projects
- ✅ Relative imports: How the dot notation works (and when to use it)
- ✅ How external packages like
numpywork differently from your own modules - ✅ Common import errors and how to fix them
- ✅ Best practices for structuring Python projects
1.3 Prerequisites
This lecture builds on concepts from:
- Chapter 01 (Modern Python Development): Python environments,
uv, andpyproject.toml - Chapter 02 (Refactoring): Refactoring from monolith to modules
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:
- Defines functions, classes, or variables
- Can be imported into other Python files
- Has its own namespace (variable names don’t conflict across modules)
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:
- As a module (imported by other code):
from geometry import calculate_ray_line - 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:
- ✅ Run directly:
python main.py - ✅ Import elsewhere:
from main import main
2.5 How Python Finds Modules
When you write import geometry, Python searches for geometry.py in these locations in order:
- Current directory (where you run Python from)
- Directories in
PYTHONPATH(environment variable) - Virtual environment’s
site-packages/(installed packages) - 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
- ✅ A module is just a
.pyfile - ✅ Every Python file is a module
- ✅ Modules provide a namespace for functions, classes, variables
- ✅ Modules can be imported or executed as scripts
- ✅ Python searches
sys.pathto find 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:
- ❌ Don’t add logging configuration in
__init__.pyfor large projects - ❌ Don’t run expensive computations at import time
- ❌ Don’t import heavy dependencies unnecessarily
Why? Code in __init__.py runs every time the package is imported, which:
- Slows down your tests (imports happen repeatedly in test suites)
- Increases CI/CD build times
- Makes your package harder to use as a dependency
- Can cause circular import issues
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):
- ❌ Avoid
from package import *in your code - it’s generally considered bad practice - ❌ Makes code harder to read - unclear where names come from
- ❌ Can cause name conflicts - overwrites existing variables
- ❌ Performance impact - imports everything, even unused code
- ✅ Ruff linter will warn you about star imports (and you should listen!)
Why __all__ still matters:
- ✅ Documents your package’s public API (what users should import)
- ✅ Used by documentation tools like Sphinx
- ✅ Helps IDEs provide better autocomplete
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:
- Regular package: When you need initialization code, convenient imports, or explicit package boundary
- Namespace package: Simple projects, no special initialization needed
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:
- Main package:
road_profile_viewer - Sub-package:
road_profile_viewer.core - Sub-package:
road_profile_viewer.ui
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
- ✅ A package is a directory containing modules
- ✅
__init__.pyis optional in Python 3.3+ but useful for initialization and convenient imports - ✅ Packages enable hierarchical code organization
- ✅ Your
road_profile_viewerdirectory is a package - ✅ Import paths use dots to navigate the package structure:
package.subpackage.module
4. Absolute Imports: The Recommended Approach
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:
- Searches for
road_profile_viewerinsys.path - Looks for
geometry.pyinside that package - Imports the
find_intersectionfunction 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:
road_profile_vieweris a packagemainis a module inside it- Relative imports work correctly
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:
- Treat this as a module path, not a file path
- Set up the import system correctly
- Make relative imports work (if you use them)
4.6 How uv run Handles This
When you use uv run:
uv run python -m road_profile_viewer.main
uv automatically:
- Activates the virtual environment
- Ensures
road_profile_vieweris insys.path - Runs with the correct Python interpreter
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
- ✅ Use full path from package root:
from road_profile_viewer.module import function - ✅ Work regardless of how code is executed
- ✅ Recommended for most projects (including this course)
- ✅ Run modules with
python -m package.modulefor best results - ✅ Your assignment requires absolute imports for reliability
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:
.= current directory (current package)..= parent directory (parent package)...= grandparent directory (grandparent package)
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:
.geometrymeans “thegeometrymodule in the same package as me”.roadmeans “theroadmodule in the same package as me”
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:
- What package the module belongs to
- Where the parent package is located
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:
mainis a module in theroad_profile_viewerpackage- Relative imports like
from .visualizationcan be resolved
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
- ✅ Use dots to navigate:
.(current),..(parent),...(grandparent) - ✅ Only work when Python knows the module is in a package
- ✅ Break when running modules as scripts directly
- ✅ Must use
python -m package.modulefor relative imports to work - ✅ For this course: use absolute imports instead
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:
- ✅ Required by the assignment specification
- ✅ More reliable for beginners
- ✅ Better IDE support in VS Code
- ✅ Easier for peer code review
- ✅ 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
- ✅ Absolute imports: Explicit, reliable, beginner-friendly
- ✅ Relative imports: Concise, package-name agnostic, requires
-mflag - ✅ Both have trade-offs; consistency matters most
- ✅ For this course: always use absolute imports
- ✅ In the real world: Follow your team’s style guide
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:
- Searches
sys.pathin order - Finds
numpy/directory insite-packages/ - Imports
numpy/__init__.py - 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:
- Searches
sys.path - Looks for
geometry.pyorgeometry/directory in project root - Doesn’t find it (because it’s inside
road_profile_viewer/) - 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:
- Built-in modules (like
sys,os,json) - Current directory (where you run Python from)
- Directories in
PYTHONPATH(environment variable) - Virtual environment’s
site-packages/(installed packages) - System Python’s
site-packages/(global packages - usually ignored in venv) - 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:
- ✅ Distributing your package to others
- ✅ Running tests from different directories
- ✅ Using your package in multiple projects
When NOT to do this (for this course):
- ❌ Not required for the assignment
- ❌ Adds complexity for beginners
- ❌
uv runhandles this automatically
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
- ✅ External packages are installed in
site-packages/(inside your venv) - ✅
site-packages/is insys.path, so simple imports work:import numpy - ✅ Your modules are nested in a package directory, so you need the full path
- ✅ Python searches
sys.pathin order: current dir → venv → stdlib - ✅ Never name your modules the same as popular packages or stdlib modules
- ✅
uv add packageinstalls tosite-packages/,uv runensures it’s in the path
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:
- Project root is in
sys.path[0] - Virtual environment’s
site-packagesis insys.path[1] - That’s why absolute imports work!
- External packages are found in
site-packages/
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:
- Shorter import paths
- Clean public API
- Package metadata accessible
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:
- External packages are just directories of Python files in
site-packages/, just like your project could be! __path__attribute shows you exactly where a package is located (works for packages, not modules)- This is useful for debugging import issues or understanding where your dependencies are installed
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
- ✅
sys.pathdetermines where Python searches for modules - ✅ Project root and
site-packages/are insys.path - ✅
__init__.pylets you customize package imports - ✅ Relative imports only work when run with
-mflag - ✅ External packages are just directories in
site-packages/ - ✅ You can inspect import locations with
module.__file__
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:
- You’re running Python from the wrong directory
- Your current directory isn’t the project root
- Virtual environment isn’t activated (if using manual venv)
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:
- You’re importing from the wrong module
- There’s a naming conflict (maybe you have
geometry.pyin the wrong place) - The function name is misspelled
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:
- Virtual environment isn’t activated
- Dependencies haven’t been installed
- You’re using the wrong Python interpreter
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:
- ✅ Are you in the correct directory? (project root)
- ✅ Are you using
python -m package.moduleinstead ofpython file.py? - ✅ Is your virtual environment activated? (or using
uv run?) - ✅ Are dependencies installed? (run
uv sync) - ✅ Is the import path correct? (check spelling, package structure)
- ✅ Are you using absolute imports? (recommended for this course)
- ✅ Does the function/class actually exist in the module?
9.8 Key Takeaways: Import Errors
- ✅ Most import errors come from running code incorrectly or wrong directory
- ✅ Use
python -m package.modulefor reliable imports - ✅ Use
uv runto avoid environment issues - ✅ Relative imports only work when Python knows package structure
- ✅ Circular imports require code restructuring
- ✅ Always check
sys.pathwhen debugging:python -c "import sys; print(sys.path)"
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:
- ✅ All imports are absolute:
from road_profile_viewer.module import ... - ✅ No relative imports:
from .module import ... - ✅ Code runs with:
uv run python -m road_profile_viewer.main - ✅ No naming conflicts with standard library or external packages
- ✅
pyproject.tomllists all dependencies correctly - ✅ Virtual environment can be recreated with
uv sync - ✅ Code passes
ruff checkandpyright(no import errors)
10.9 Key Takeaways: Best Practices
- ✅ Consistency: Pick absolute imports and use them everywhere
- ✅ Clarity: Use specific imports, avoid
import * - ✅ Organization: Group imports logically (stdlib, external, local)
- ✅ Reliability: Run with
python -moruv run - ✅ Safety: Never shadow standard library names
- ✅ Simplicity: Keep
__init__.pyminimal - ✅ Documentation: Explain dependencies in module docstrings
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
- ✅ Modules: Every
.pyfile is a module with its own namespace - ✅ Packages: Directories containing modules, organized hierarchically
- ✅
__init__.py: Optional in Python 3.3+ but useful for initialization and convenient imports - ✅ Absolute Imports: Use full paths from package root (
from road_profile_viewer.geometry import ...) - ✅ Relative Imports: Use dots to navigate package hierarchy (
.= current,..= parent) - ✅ External Packages: Installed in
site-packages/, importable with simple names (import numpy) - ✅ sys.path: Python’s search path determines where modules are found
- ✅ Common Errors: How to diagnose and fix import issues
- ✅ Best Practices: Guidelines for professional Python projects
11.2 Why This Matters for Your Career
Understanding Python’s import system is fundamental because:
- ✅ Every real-world Python project uses multiple modules and packages
- ✅ Collaborating with teams requires consistent import conventions
- ✅ Debugging import errors is a daily task for professional developers
- ✅ Structuring large codebases depends on proper package organization
- ✅ Contributing to open source means reading and understanding existing import structures
11.3 How This Applies to Your Assignment
For the road-profile-viewer refactoring assignment:
- ✅ Use absolute imports exclusively:
from road_profile_viewer.module import function - ✅ Run with
python -m road_profile_viewer.mainoruv run python -m road_profile_viewer.main - ✅ Keep your package structure flat (no need for subdirectories)
- ✅ Optionally add
__init__.pyfor package metadata and convenient imports - ✅ 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:
- PEP 420 - Implicit Namespace Packages: https://peps.python.org/pep-0420/
- Python Import System Documentation: https://docs.python.org/3/reference/import.html
- Real Python - Absolute vs Relative Imports: https://realpython.com/absolute-vs-relative-python-imports/
- PEP 8 - Import Style Guide: https://peps.python.org/pep-0008/#imports
11.5 Resources for the Assignment
- Road Profile Viewer Template: https://github.com/hs-aalen-software-engineering/road-profile-viewer-class-template
- Chapter 02 (Refactoring) - Refactoring: Review the lecture on refactoring from monolith to modules
- uv Documentation: https://docs.astral.sh/uv/
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:
- Branch 1: Pure absolute imports
- Branch 2: Pure relative imports
- Branch 3: Mixed (see what breaks!)
12. Next Steps
Now that you understand Python’s import system, you’re ready to:
- ✅ Complete your road-profile-viewer refactoring with confidence
- ✅ Structure larger Python projects professionally
- ✅ Debug import errors quickly and effectively
- ✅ Collaborate with teams using consistent conventions
- ✅ 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.