pathlint 0.2.0
Fast linter to detect os.path usage and encourage pathlib adoption
License: MIT
Requires Python: >=3.9
Keywords: code-quality, linter, os.path, pathlib, pre-commit, python-linter

# pathlint

[![PyPI version](https://badge.fury.io/py/pathlint.svg)](https://pypi.org/project/pathlint/)
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)

> Fast linter to detect os.path usage and encourage pathlib adoption

## Why pathlint?

Still using `os.path` in 2025+? `pathlint` is a fast, comprehensive linter that detects **all** `os.path` usage patterns in your Python codebase and encourages migration to the modern `pathlib` module.

### Key Features

- **Comprehensive Detection**: Catches import statements, aliased imports, function calls, and even type annotations
- **Performance Optimized**: 3x faster than traditional AST-based linters with early termination
- **Smart Exclusions**: Automatically skips `venv`, `__pycache__`, `node_modules`, and other common directories
- **Professional Output**: Clean, informative output with optional statistics
- **Auto-fix Support**: Experimental auto-fixer to migrate code automatically

## Installation

```bash
pip install pathlint
```

## Pre-commit Hook Integration

pathlint integrates with [pre-commit](https://pre-commit.com/) to catch os.path usage before commits.

### Setup

Add to your `.pre-commit-config.yaml`:

```yaml
repos:
  - repo: https://github.com/pszemraj/pathlint
    rev: v0.1.0  # Use latest release
    hooks:
      - id: pathlint
```

Install the hook:

```bash
pre-commit install
```

### Usage

Runs automatically on `git commit`. Manual check:

```bash
pre-commit run pathlint --all-files
```

### Auto-fix Mode (Experimental)

```yaml
- id: pathlint-fix  # Use auto-fix hook instead
```

⚠️ Always review auto-fixed changes!

## Usage

### Basic Linting

```bash
# Lint files or directories
pathlint myfile.py
pathlint src/
pathlint .

# Multiple paths
pathlint src/ tests/ scripts/
```

### Advanced Options

```bash
# Show statistics about worst offenders
pathlint . --stats

# Aggressive mode (for fun)
pathlint . --aggressive

# Auto-fix mode (experimental)
pathlint --dry-run src/  # Preview changes
pathlint --fix src/      # Apply fixes
```

## What It Detects

pathlint catches ALL these patterns:

```python
# Import patterns
import os.path
import os.path as ospath
from os import path
from os import path as p
from os.path import join, exists

# Direct usage
os.path.exists('file.txt')
os.path.join('dir', 'file')
path.dirname(__file__)  # After 'from os import path'

# Type annotations (missed by most linters!)
def process(f: os.path.PathLike):
    pass

def get_path() -> os.path.PathLike:
    return 'test'
```

## Output Format

### Clean Files

```
✓ 42 files checked - no os.path usage found!
```

### Files with Issues

```
/path/to/file.py
  L   1: import os.path
  L  23: x = os.path.join('a', 'b')
  L  45: def process(f: os.path.PathLike):

────────────────────────────────────────
Files checked:     42
Files with issues: 3
Total violations:  7

✗ Found os.path usage. Migrate to pathlib.
```

### With Statistics (`--stats`)

```
Worst offenders:
   12 - legacy_utils.py
    5 - old_config.py
    2 - setup.py
```

## Exit Codes

- `0` - No os.path usage found
- `1` - os.path usage detected
- `2` - Error (no files found, invalid paths, etc.)

## Performance

Optimized for speed with:

- Early termination for files without 'os' or 'path' strings
- Smart directory traversal with automatic exclusions
- Single-pass AST visitor
- Automatic deduplication of findings

Benchmarks on real codebases:

```
100 files: 0.31s (vs 0.84s traditional)
500 files: 1.1s (vs 4.2s traditional)
```

## Auto-fix (Experimental)

Pathlint can automatically migrate common os.path patterns:

```bash
# Preview changes without modifying files
pathlint --dry-run myfile.py

# Apply fixes (modifies files!)
pathlint --fix myfile.py

# Fix entire directory
pathlint --fix src/
```

Supports migration of:

- Import statements
- Common function calls (`exists`, `join`, `dirname`, etc.)
- Path attributes
- Automatic `pathlib` import addition

**⚠️ Warning**: Always review auto-fixed code and test thoroughly!

## Development

```bash
# Install with dev dependencies
pip install -e .[dev]

# Run tests
pytest

# Format code
ruff format .

# Check linting
ruff check --fix .
```

## Why Pathlib?

`pathlib` provides:

- Object-oriented interface
- Operator overloading (`/` for joining paths)
- Cross-platform compatibility
- Rich path manipulation methods
- Type safety with `Path` objects

```python
# Old way (os.path)
import os.path
filepath = os.path.join(os.path.dirname(__file__), 'data', 'config.json')
if os.path.exists(filepath):
    abs_path = os.path.abspath(filepath)

# Modern way (pathlib)
from pathlib import Path
filepath = Path(__file__).parent / 'data' / 'config.json'
if filepath.exists():
    abs_path = filepath.resolve()
```

**Note**: While pathlib is recommended for most use cases, there are rare scenarios where `os.path` might offer better performance[^1].

[^1]: In extremely performance-critical code paths dealing with millions of file operations, `os.path` string operations can be marginally faster than Path object instantiation. However, these edge cases are rare and should only be considered after profiling confirms a bottleneck.

## License

MIT License - see LICENSE.txt

## Contributing

Contributions welcome! Please ensure:

1. Tests pass: `pytest`
2. Code is formatted: `ruff format .`
3. No linting errors: `ruff check .`

---

**Remember**: Friends don't let friends use `os.path` in 2025+!