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

🔄 正在渲染 Mermaid 图表...

Python Import Priority

PriorityImport TypeExampleDescription
1Standard Libraryimport os, sysPython built-in standard library
2Third-Partyimport requests, numpyPackages installed via pip
3Local Modulesfrom .utils import helperProject 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
  1. Follow PEP8 import order: Standard library → Third-party → Local modules
  2. Explicit imports: Avoid wildcard imports (from module import *)
  3. Relative import conventions: Use relative imports within packages, absolute imports from outside
  4. Lazy imports: Only use function-level imports when necessary
  5. Circular dependency detection: Regularly check for and refactor circular imports
Considerations
  1. Performance impact: Excessive imports can affect startup time
  2. Namespace pollution: Avoid wildcard imports polluting namespace
  3. Version compatibility: Handle import compatibility across different library versions
  4. 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.