A Python tool that automatically sorts class methods by visibility (public, protected, private) and type (class, static, instance).
- Automatically reorders class methods based on visibility and method type
- Two-level sorting: primary by visibility, secondary by method type
- Fully configurable ordering via
pyproject.toml - Pre-commit hook integration
- Colored output for better readability
- Check mode for CI/CD validation
- Diff mode to preview changes
# Using uv (recommended)
uv add undersort
# Using pip
pip install undersort
# For development
git clone https://github.com/kivicode/undersort
cd undersort
uv syncConfigure the method ordering in your pyproject.toml:
[tool.undersort]
# Method visibility ordering (primary sort)
# Options: "public", "protected", "private"
order = ["public", "protected", "private"]
# Method type ordering within each visibility level (secondary sort, optional)
# Options: "class" (classmethod), "static" (staticmethod), "instance" (regular methods)
# Default: ["instance", "class", "static"]
method_type_order = ["instance", "class", "static"]
# Exclude files/directories matching these patterns (optional)
# Patterns support glob syntax (e.g., "tests/*", "migrations/*.py", "**/generated/*")
# exclude = ["tests/*", "migrations/*.py"]- Public methods: No underscore prefix (e.g.,
def method()) or magic methods (e.g.,__init__,__str__) - Protected methods: Single underscore prefix (e.g.,
def _method()) - Private methods: Double underscore prefix, not magic (e.g.,
def __method())
- Class methods: Decorated with
@classmethod - Static methods: Decorated with
@staticmethod - Instance methods: Regular methods (no special decorator)
Methods are sorted in two levels:
- Primary: By visibility (public → protected → private)
- Secondary: Within each visibility level, by method type (instance → class → static by default)
The sorting algorithm minimizes movement to preserve the original order as much as possible:
- Methods that need to move DOWN (to a later section) are placed at the beginning of their target section
- Methods that need to move UP (to an earlier section) are placed at the end of their target section
- Methods already in the correct section maintain their relative order
Example order with default configuration:
- Public instance methods
- Public class methods
- Public static methods
- Protected instance methods
- Protected class methods
- Protected static methods
- Private instance methods
- Private class methods
- Private static methods
You can prevent sorting at different levels using # nosort comments (case-insensitive):
File-level: Skip entire file
# nosort: file
class Example:
def _protected(self):
pass
def public(self):
pass # File won't be sortedClass-level: Skip specific class
class Example: # nosort
def _protected(self):
pass
def public(self):
pass # This class won't be sorted
class Other:
def _protected(self):
pass
def public(self):
pass # This class WILL be sortedMethod-level: Keep method in its current position
class Example:
def public_a(self):
pass
def _protected(self): # nosort
pass # Stays here, between public methods
def public_b(self):
pass # Will move up, but _protected stays in place# Sort a single file
undersort example.py
# Sort multiple files
undersort file1.py file2.py file3.py
# Sort all Python files in a directory (recursive by default)
undersort src/
# Sort all Python files in current directory and subdirectories
undersort .
# Non-recursive directory sorting (only files in the directory, not subdirectories)
undersort src/ --no-recursive
# Wildcards work too (expanded by shell)
undersort *.py
undersort src/**/*.py
# Check if files need sorting (useful for CI)
undersort --check example.py
undersort --check src/
# Show diff of changes
undersort --diff example.py
# Combine flags
undersort --check --diff src/
# Exclude specific files or directories
undersort --exclude "tests/*" --exclude "migrations/*.py" src/
# Multiple exclude patterns (can be combined with config file patterns)
undersort --exclude "test_*.py" --exclude "*/legacy/*" .Note: By default, undersort excludes all dot-prefixed directories (e.g., .venv, .git, .pytest_cache) and common build directories (venv, __pycache__, node_modules) when scanning directories recursively. You can add custom exclusions via CLI flags or the config file.
Add to your .pre-commit-config.yaml:
repos:
- repo: local
hooks:
- id: undersort
name: undersort
entry: undersort
language: python
types: [python]
additional_dependencies: ["undersort"]Then install the hook:
pip install pre-commit
pre-commit installclass Example:
def _protected_instance(self):
pass
@staticmethod
def public_static():
pass
def __init__(self):
pass
@classmethod
def _protected_class(cls):
pass
def public_instance(self):
pass
def __private_method(self):
pass
@classmethod
def public_class(cls):
passclass Example:
def __init__(self):
pass
def public_instance(self):
pass
@classmethod
def public_class(cls):
pass
@staticmethod
def public_static():
pass
def _protected_instance(self):
pass
@classmethod
def _protected_class(cls):
pass
def __private_method(self):
passThe methods are now organized by:
- Visibility: public (including
__init__) → protected → private - Type (within each visibility): instance → class → static
# Install dependencies
uv sync
# Run on example file
uv run undersort example.py
# Test with check mode
uv run undersort --check example.py
# View diff
uv run undersort --diff example.pyMIT