Skip to content

Python: Paths and Module Resolution Guide

A comprehensive guide to working with file paths and module imports in Python, covering both filesystem navigation with pathlib and module resolution with PYTHONPATH and sys.path.

Part 1: Filesystem Navigation with pathlib

Section titled “Part 1: Filesystem Navigation with pathlib”

The pathlib module provides an object-oriented interface for working with filesystem paths.

from pathlib import Path
current = Path('.')
my_file = Path('/home/user/docs/file.txt')
absolute = Path(__file__).resolve()
from pathlib import Path
# Go up one or more directories
parent = Path(__file__).resolve().parent
grandparent = Path(__file__).resolve().parents[1]
# Move into a subdirectory
subdir = parent / "my_folder"

The / operator makes path construction intuitive and cross-platform compatible.

p = Path("example.txt")
p.exists() # Check if path exists
p.is_file() # Is it a file?
p.is_dir() # Is it a directory?
p.is_symlink() # Is it a symbolic link?
p.is_absolute() # Is it an absolute path?
# Read and write files
file = Path("example.txt")
file.write_text("Hello, pathlib!")
content = file.read_text()
print(content)
# Binary mode
data = file.read_bytes()
file.write_bytes(b'\x00\x01\x02')
# Iterate over a directory
for path in Path('.').iterdir():
print(path)
# Globbing patterns
for py_file in Path('.').rglob('*.py'):
print(py_file)
# Create directories
Path('new/nested/dirs').mkdir(parents=True, exist_ok=True)
p = Path('/home/user/docs/file.txt')
p.name # 'file.txt'
p.stem # 'file'
p.suffix # '.txt'
p.parent # Path('/home/user/docs')
p.parts # ('/', 'home', 'user', 'docs', 'file.txt')

Understanding how Python finds and imports modules is crucial for debugging import issues and managing dependencies.

PYTHONPATH is an environment variable that tells Python where to look for importable modules.

Terminal window
# View current PYTHONPATH
echo $PYTHONPATH
# Set PYTHONPATH temporarily
export PYTHONPATH=/path/to/modules:$PYTHONPATH
# Check Python's actual search path
python -c "import sys; print('\n'.join(sys.path))"

Python automatically adds directories listed in PYTHONPATH to sys.path at runtime, making modules in those directories importable.

When multiple versions of a package exist in sys.path, Python searches in order and uses the first match found. This is critical for understanding version conflicts.

Create two versions of the same module:

File: ~/package1/package.py

def version():
return "Version 1"

File: ~/package2/package.py

def version():
return "Version 2"

Test the resolution order:

Terminal window
# Set PYTHONPATH with package1 first
PYTHONPATH=~/package1:~/package2 python
# In the Python interpreter
import package
print(package.version()) # Output: "Version 1"
import sys
print(sys.path) # Shows package1 listed before package2

Python loads package1 because it appears first in sys.path.

Python’s module caching and path resolution can be manipulated for advanced use cases.

import os
import sys
MODULE = 'package'
def print_package_version():
import package
print(f"Currently loaded package version: {package.version()}")
# Check if module already loaded
if 'package' in sys.modules:
print_package_version() # Version 1
# Remove cached module to force reimport
del sys.modules[MODULE]
# Prioritize Version 2 by inserting its path at the beginning
# sys.path was ["", "package1", "package2"]
# Now becomes ["", "package2", "package1"]
sys.path.insert(1, os.path.expanduser("~/package2"))
print_package_version() # Now outputs Version 2

Technique 2: Loading Multiple Versions Simultaneously

Section titled “Technique 2: Loading Multiple Versions Simultaneously”

Use importlib to load specific module files, allowing different versions in the same script.

import importlib.util
# Load Version 1 explicitly by file path
spec1 = importlib.util.spec_from_file_location('package_v1', '~/package1/package.py')
package_v1 = importlib.util.module_from_spec(spec1)
spec1.loader.exec_module(package_v1)
# Version 2 already loaded through normal import
import package as package_v2
print(package_v1.version()) # Output: "Version 1"
print(package_v2.version()) # Output: "Version 2"

Override the cached module in sys.modules to change what gets imported globally.

import sys
import importlib.util
MODULE = 'package'
# Load Version 1 from specific file
spec = importlib.util.spec_from_file_location(MODULE, '~/package1/package.py')
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
# Replace the globally cached module
if MODULE in sys.modules:
del sys.modules[MODULE]
sys.modules[MODULE] = mod
# Now all future imports get Version 1
import package
print(package.version()) # Output: "Version 1"

Part 3: Combining pathlib and Module Resolution

Section titled “Part 3: Combining pathlib and Module Resolution”

Use pathlib to dynamically add directories to sys.path:

from pathlib import Path
import sys
# Add parent directory to sys.path
project_root = Path(__file__).resolve().parent.parent
if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
# Now you can import from project root
from mypackage import mymodule
  1. Use pathlib over os.path - More readable and cross-platform
  2. Use / operator - Cleaner than string concatenation
  3. Use .resolve() - Get absolute paths to avoid ambiguity
  4. Check .exists() - Verify paths before operations

Warning: Manipulating sys.path and sys.modules can lead to hard-to-debug issues. Use these techniques sparingly:

  • For testing: Mock different module versions in unit tests
  • For development: Work with local package versions alongside installed ones
  • For debugging: Investigate module loading issues

Better alternatives:

  1. Use virtual environments - Isolate dependencies per project
  2. Use pip install -e . - Install local packages in development mode
  3. Configure PYTHONPATH in shell profile - Rather than modifying sys.path at runtime
  4. Use package management tools - Like poetry, pipenv, or uv for dependency management
# ❌ Wrong: Mixing pathlib and string operations
path = Path('dir') + '/file.txt' # TypeError
# ✅ Correct: Use the / operator
path = Path('dir') / 'file.txt'
# ❌ Wrong: Adding relative paths
sys.path.append('relative/path') # May not work as expected
# ✅ Correct: Use absolute paths
sys.path.append(str(Path('relative/path').resolve()))
# ❌ Wrong: Modifying sys.path after import
import mymodule
sys.path.insert(0, '/new/path')
import mymodule # Still uses old cached version
# ✅ Correct: Modify sys.path before import
sys.path.insert(0, '/new/path')
import mymodule
OperationCode
Current directoryPath('.')
Absolute pathPath(__file__).resolve()
Parent directorypath.parent
Join pathspath / 'subdir' / 'file.txt'
Check existspath.exists()
Read textpath.read_text()
Write textpath.write_text(content)
List directorylist(path.iterdir())
Find fileslist(path.rglob('*.py'))
OperationCode
View search pathimport sys; print(sys.path)
Add to frontsys.path.insert(0, '/path')
Add to endsys.path.append('/path')
Check loaded modulessys.modules.keys()
Remove cached moduledel sys.modules['module']
Reload moduleimportlib.reload(module)