Chapter 6: Import Analysis and Dependency Management
Haiyue
22min
Chapter 6: Import Analysis and Dependency Management
Learning Objectives
- Master best practices for Python’s import system
- Understand Pylint’s import checking capabilities
- Learn to analyze and optimize module dependencies
- Master detecting and resolving circular imports
Knowledge Points
Import-Related Check Types
🔄 正在渲染 Mermaid 图表...
Python Import Priority
| Priority | Import Type | Example | Description |
|---|---|---|---|
| 1 | Standard Library | import os, sys | Python built-in standard library |
| 2 | Third-Party | import requests, numpy | Packages installed via pip |
| 3 | Local Modules | from .utils import helper | Project internal modules |
Example Code
Import Order and Style Guidelines
# Incorrect import order and style
import requests # Third-party library
import os # Standard library - wrong order
from myproject.utils import helper # Local module
import sys # Standard library - wrong position
from numpy import array # Third-party library - wrong position
def some_function():
import json # C0415: import-outside-toplevel
return json.dumps({})
# Correct import order and style
"""
Module functionality description
This module demonstrates correct import style and order.
"""
# 1. Standard library imports
import json
import os
import sys
from pathlib import Path
from typing import Dict, List, Optional
# 2. Third-party library imports
import numpy as np
import pandas as pd
import requests
from flask import Flask, request
# 3. Local module imports
from myproject.config import settings
from myproject.database import DatabaseManager
from myproject.utils import helper_function
# Module-level constants
DEFAULT_TIMEOUT = 30
MAX_RETRIES = 3
class ApiClient:
"""API client class"""
def __init__(self):
self.session = requests.Session()
self.base_url = settings.API_BASE_URL
def fetch_data(self) -> Dict:
"""Fetch data"""
# All imports are at module top
response = self.session.get(
f"{self.base_url}/data",
timeout=DEFAULT_TIMEOUT
)
return response.json()
Handling Import Errors
# Import error examples and solutions
# E0401: import-error - module doesn't exist
try:
import nonexistent_module # Triggers import-error
except ImportError:
nonexistent_module = None
# Better handling approach
try:
import optional_dependency
HAS_OPTIONAL_DEPENDENCY = True
except ImportError:
HAS_OPTIONAL_DEPENDENCY = False
optional_dependency = None
def use_optional_feature():
"""Use optional dependency feature"""
if not HAS_OPTIONAL_DEPENDENCY:
raise ImportError("optional_dependency needs to be installed")
return optional_dependency.some_function()
# E0611: no-name-in-module - specified name doesn't exist in module
try:
from json import nonexistent_function # Triggers error
except ImportError:
def nonexistent_function():
raise NotImplementedError("Function not available")
# Correct import verification
def safe_import_from_module():
"""Safely import module members"""
import json
# Check if attribute exists
if hasattr(json, 'dumps'):
return json.dumps
else:
raise ImportError("json.dumps not available")
Handling Unused Imports
# W0611: unused-import - unused import
# Incorrect example: import but not used
import os # Not used
import sys # Not used
import json # Used
from typing import List, Dict, Optional # Optional not used
def process_data(data: List[Dict]) -> str:
"""Process data"""
return json.dumps(data)
# Solution 1: Remove unused imports
import json
from typing import Dict, List
def process_data(data: List[Dict]) -> str:
"""Process data"""
return json.dumps(data)
# Solution 2: Add noqa comment for conditionally used imports
import os # noqa: F401 # Used in some conditions
import sys # pylint: disable=unused-import # Dynamically used
def get_platform_info():
"""Get platform information"""
if sys.platform.startswith('win'):
return os.path.join('C:', 'Windows')
return '/usr/local'
# Solution 3: Use __all__ to control exports
import logging
import threading
from datetime import datetime
__all__ = ['Logger', 'get_timestamp'] # Explicitly define exports
class Logger:
"""Logger"""
def __init__(self):
self.logger = logging.getLogger(__name__)
self.lock = threading.Lock()
def log(self, message: str):
"""Log message"""
with self.lock:
self.logger.info(f"{datetime.now()}: {message}")
def get_timestamp() -> str:
"""Get timestamp"""
return datetime.now().isoformat()
Wildcard Import Issues
# W0614: unused-wildcard-import - unused wildcard import
# Incorrect example: wildcard imports
from math import * # Import all, but might only use part
from os.path import * # Pollutes namespace
from mymodule import * # Unclear what is imported
def calculate_area(radius):
"""Calculate circle area"""
return pi * radius ** 2 # pi comes from math.*
# Solution 1: Explicitly import what you need
import math
from os.path import join, exists
from mymodule import specific_function, SpecificClass
def calculate_area(radius):
"""Calculate circle area"""
return math.pi * radius ** 2
def process_file(filename):
"""Process file"""
filepath = join('/tmp', filename)
if exists(filepath):
return specific_function(filepath)
return None
# Solution 2: Use aliases to avoid conflicts
import math as m
import numpy as np
from mymodule import helper as my_helper
def scientific_calculation(data):
"""Scientific calculation"""
# Clearly know where functions come from
result1 = m.sqrt(data)
result2 = np.sqrt(data)
result3 = my_helper.process(data)
return result1, result2, result3
# Solution 3: Controlled wildcard import (only use when module is designed for it)
# In constants.py
PI = 3.14159
E = 2.71828
GOLDEN_RATIO = 1.618
__all__ = ['PI', 'E', 'GOLDEN_RATIO'] # Explicitly define exports
# In using module
from constants import * # Safe here because __all__ controls it
def calculate_circle_area(radius):
"""Calculate circle area"""
return PI * radius ** 2
Circular Import Detection and Resolution
# R0401: cyclic-import - circular import
# Problem example: circular import
# file: models/user.py
from models.order import Order # Import Order
class User:
"""User model"""
def __init__(self, user_id):
self.user_id = user_id
self.orders = []
def add_order(self, order: Order):
"""Add order"""
self.orders.append(order)
# file: models/order.py
from models.user import User # Import User - creates cycle
class Order:
"""Order model"""
def __init__(self, order_id, user: User):
self.order_id = order_id
self.user = user
# Solution 1: Use string form of type annotations (deferred evaluation)
# file: models/user.py
from typing import TYPE_CHECKING, List
if TYPE_CHECKING:
from models.order import Order # Only import during type checking
class User:
"""User model"""
def __init__(self, user_id: str):
self.user_id = user_id
self.orders: List['Order'] = [] # Use string form of type annotation
def add_order(self, order: 'Order'):
"""Add order"""
self.orders.append(order)
# file: models/order.py
from models.user import User
class Order:
"""Order model"""
def __init__(self, order_id: str, user: User):
self.order_id = order_id
self.user = user
# Solution 2: Refactor to common base module
# file: models/base.py
from typing import Protocol
class UserProtocol(Protocol):
"""User protocol"""
user_id: str
class OrderProtocol(Protocol):
"""Order protocol"""
order_id: str
user: UserProtocol
# file: models/user.py
from typing import List
from models.base import OrderProtocol
class User:
"""User model"""
def __init__(self, user_id: str):
self.user_id = user_id
self.orders: List[OrderProtocol] = []
def add_order(self, order: OrderProtocol):
"""Add order"""
self.orders.append(order)
# file: models/order.py
from models.base import UserProtocol
class Order:
"""Order model"""
def __init__(self, order_id: str, user: UserProtocol):
self.order_id = order_id
self.user = user
# Solution 3: Use dependency injection
# file: services/user_service.py
class UserService:
"""User service"""
def __init__(self):
self.users = {}
def create_user(self, user_id: str):
"""Create user"""
from models.user import User
user = User(user_id)
self.users[user_id] = user
return user
def add_order_to_user(self, user_id: str, order):
"""Add order to user"""
if user_id in self.users:
self.users[user_id].add_order(order)
# file: services/order_service.py
class OrderService:
"""Order service"""
def __init__(self, user_service):
self.user_service = user_service
self.orders = {}
def create_order(self, order_id: str, user_id: str):
"""Create order"""
from models.order import Order
user = self.user_service.users.get(user_id)
if user:
order = Order(order_id, user)
self.orders[order_id] = order
self.user_service.add_order_to_user(user_id, order)
return order
return None
Relative vs Absolute Imports
# Project structure example
"""
myproject/
├── __init__.py
├── main.py
├── config/
│ ├── __init__.py
│ └── settings.py
├── utils/
│ ├── __init__.py
│ ├── helpers.py
│ └── validators.py
└── api/
├── __init__.py
├── views.py
└── models.py
"""
# Incorrect import approach
# file: api/views.py
import config.settings # May have issues
from utils import helpers # May have issues
import api.models # Self-reference
# Correct absolute imports
# file: api/views.py
from myproject.config import settings
from myproject.utils import helpers
from myproject.api import models
class ApiView:
"""API view class"""
def __init__(self):
self.config = settings.get_config()
self.helper = helpers.ApiHelper()
# Correct relative imports (use within package)
# file: api/views.py
from ..config import settings # Relative import parent package
from ..utils import helpers # Relative import sibling package
from . import models # Relative import sibling module
class ApiView:
"""API view class"""
def __init__(self):
self.config = settings.get_config()
self.helper = helpers.ApiHelper()
self.model = models.ApiModel()
# Best practices for relative imports
# file: utils/helpers.py
from .validators import validate_email, validate_phone # Sibling module
from ..config.settings import DEBUG_MODE # Parent package
class ApiHelper:
"""API helper class"""
def process_user_input(self, email: str, phone: str):
"""Process user input"""
if DEBUG_MODE:
print(f"Processing: {email}, {phone}")
if not validate_email(email):
raise ValueError("Invalid email format")
if not validate_phone(phone):
raise ValueError("Invalid phone format")
return {'email': email, 'phone': phone}
Dynamic Import Handling
# Correct handling of dynamic imports
import importlib
from typing import Any, Optional
class PluginManager:
"""Plugin manager"""
def __init__(self):
self.plugins = {}
self.loaded_modules = {}
def load_plugin(self, plugin_name: str, module_path: str) -> Optional[Any]:
"""Dynamically load plugin"""
try:
# Use importlib for dynamic import
module = importlib.import_module(module_path)
# Verify module has expected interface
if not hasattr(module, 'Plugin'):
raise ImportError(f"{module_path} doesn't have Plugin class")
plugin_class = getattr(module, 'Plugin')
plugin_instance = plugin_class()
# Validate plugin interface
required_methods = ['initialize', 'execute', 'cleanup']
for method in required_methods:
if not hasattr(plugin_instance, method):
raise ImportError(f"Plugin missing required method: {method}")
self.plugins[plugin_name] = plugin_instance
self.loaded_modules[plugin_name] = module
return plugin_instance
except ImportError as e:
print(f"Failed to load plugin {plugin_name}: {e}")
return None
def reload_plugin(self, plugin_name: str):
"""Reload plugin"""
if plugin_name in self.loaded_modules:
module = self.loaded_modules[plugin_name]
importlib.reload(module)
# Recreate plugin instance
plugin_class = getattr(module, 'Plugin')
self.plugins[plugin_name] = plugin_class()
# Conditional import handling
def get_json_encoder():
"""Get JSON encoder"""
# Prefer faster ujson
try:
import ujson as json_module
encoder_type = 'ujson'
except ImportError:
try:
import orjson as json_module
encoder_type = 'orjson'
except ImportError:
import json as json_module
encoder_type = 'json'
return json_module, encoder_type
# Version-compatible import handling
def import_with_fallback():
"""Import with fallback"""
try:
# Python 3.8+
from functools import cached_property
PropertyCache = cached_property
except ImportError:
# Python < 3.8 compatibility implementation
class PropertyCache:
def __init__(self, func):
self.func = func
self.__doc__ = func.__doc__
def __get__(self, obj, cls):
if obj is None:
return self
value = self.func(obj)
setattr(obj, self.func.__name__, value)
return value
return PropertyCache
Import Analysis and Optimization Tool
# Import analysis and optimization tool
import ast
import sys
from pathlib import Path
from typing import Dict, List, Set
class ImportAnalyzer:
"""Import analyzer"""
def __init__(self, project_root: str):
self.project_root = Path(project_root)
self.imports_map: Dict[str, Set[str]] = {}
self.unused_imports: Dict[str, List[str]] = {}
def analyze_file(self, file_path: Path) -> Dict:
"""Analyze imports in a single file"""
with open(file_path, 'r', encoding='utf-8') as f:
tree = ast.parse(f.read())
imports = []
used_names = set()
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
imports.append({
'type': 'import',
'module': alias.name,
'alias': alias.asname,
'line': node.lineno
})
elif isinstance(node, ast.ImportFrom):
for alias in node.names:
imports.append({
'type': 'from_import',
'module': node.module,
'name': alias.name,
'alias': alias.asname,
'line': node.lineno
})
elif isinstance(node, ast.Name):
used_names.add(node.id)
return {
'imports': imports,
'used_names': used_names,
'file_path': str(file_path)
}
def find_unused_imports(self, analysis: Dict) -> List[str]:
"""Find unused imports"""
unused = []
imports = analysis['imports']
used_names = analysis['used_names']
for imp in imports:
if imp['type'] == 'import':
name = imp['alias'] or imp['module'].split('.')[0]
if name not in used_names:
unused.append(f"Line {imp['line']}: import {imp['module']}")
elif imp['type'] == 'from_import':
name = imp['alias'] or imp['name']
if name not in used_names and imp['name'] != '*':
unused.append(f"Line {imp['line']}: from {imp['module']} import {imp['name']}")
return unused
def analyze_project(self) -> Dict:
"""Analyze entire project"""
results = {}
for py_file in self.project_root.rglob('*.py'):
if '__pycache__' in str(py_file):
continue
try:
analysis = self.analyze_file(py_file)
unused = self.find_unused_imports(analysis)
results[str(py_file)] = {
'analysis': analysis,
'unused_imports': unused
}
except Exception as e:
print(f"Error analyzing file {py_file}: {e}")
return results
# Usage example
def main():
"""Main function"""
if len(sys.argv) != 2:
print("Usage: python import_analyzer.py <project_path>")
return
project_path = sys.argv[1]
analyzer = ImportAnalyzer(project_path)
results = analyzer.analyze_project()
print("=== Import Analysis Report ===")
for file_path, data in results.items():
unused = data['unused_imports']
if unused:
print(f"\nFile: {file_path}")
for item in unused:
print(f" Unused import: {item}")
if __name__ == "__main__":
main()
Configuring Import Check Rules
# Import-related configuration in .pylintrc
"""
[IMPORTS]
# Known third-party libraries (for import order checking)
known-third-party = requests,numpy,pandas,flask,django
# Known standard library modules
known-standard-library = os,sys,json,re,math
# Analyze fallback blocks
analyse-fallback-blocks = no
# Import graph output format
import-graph =
# External import graph output format
ext-import-graph =
# Internal import graph output format
int-import-graph =
# Preferred module priority order
preferred-modules =
# Allow wildcard imports from modules
allow-wildcard-with-all = no
# Minimum common module
init-import = no
# Enforce import order
import-order-style = pep8
[MESSAGES CONTROL]
# Disable specific import-related checks
disable =
import-error, # May need to disable in some environments
no-name-in-module, # May need to disable for dynamic modules
unused-import, # May need to keep in some cases
"""
# Project-specific import configuration
def configure_import_rules():
"""Configure project-specific import rules"""
# Web project configuration
web_project_config = {
'known-third-party': [
'flask', 'django', 'fastapi', 'requests',
'sqlalchemy', 'redis', 'celery'
],
'import-order-style': 'pep8',
'allow-wildcard-with-all': False
}
# Data science project configuration
datascience_config = {
'known-third-party': [
'numpy', 'pandas', 'scipy', 'sklearn',
'matplotlib', 'seaborn', 'jupyter'
],
'allow-wildcard-with-all': True, # Some scientific libraries may need this
'preferred-modules': ['numpy:np', 'pandas:pd']
}
# Microservice project configuration
microservice_config = {
'known-third-party': [
'fastapi', 'pydantic', 'uvicorn', 'httpx',
'docker', 'kubernetes', 'prometheus'
],
'analyse-fallback-blocks': True,
'import-order-style': 'pep8'
}
return {
'web_project': web_project_config,
'datascience': datascience_config,
'microservice': microservice_config
}
Import Best Practices
- Follow PEP8 import order: Standard library → Third-party → Local modules
- Explicit imports: Avoid wildcard imports (
from module import *) - Relative import conventions: Use relative imports within packages, absolute imports from outside
- Lazy imports: Only use function-level imports when necessary
- Circular dependency detection: Regularly check for and refactor circular imports
Considerations
- Performance impact: Excessive imports can affect startup time
- Namespace pollution: Avoid wildcard imports polluting namespace
- Version compatibility: Handle import compatibility across different library versions
- Dynamic import risks: Use dynamic imports cautiously with proper error handling
Good import management is an important foundation for Python project maintainability. Pylint’s import checks effectively improve code quality.