Sort class methods and module-level functions into configurable, regex-matched groups
Project description
funcsort
A Python tool that automatically sorts class methods and module-level functions into configurable, regex-matched groups.
funcsort ships with a default configuration that reproduces its classic behaviour — sorting class methods by visibility (creational → dunder → public → protected → private) and type (instance → class → static) — but the engine underneath is fully generic: you define your own ordered groups, match member names with regular expressions, and control sorting at both class and module scope.
Features
- Generic, configuration-driven engine with a behaviour-preserving default
- Define your own groups and ordering; match member names with regex
- Sort both class methods and module-level functions
- Optionally sort module-level assignments/constants by opting a group into them
- Per-group filters by member kind, method type and scope
- Configurable via a dedicated
funcsort.tomlor[tool.funcsort]inpyproject.toml - Pre-commit hook integration, colored output, check mode (CI) and diff mode
Installation
# Using uv (recommended)
uv add funcsort
# Using pip
pip install funcsort
# For development
git clone https://github.com/HEROgold/funcsort
cd funcsort
uv sync
Configuration
funcsort reads configuration from a dedicated funcsort.toml if present, otherwise
from the [tool.funcsort] table of pyproject.toml. Both use the same [tool.funcsort]
section and keys.
Defaults and scalar settings
With no configuration, funcsort uses its built-in default groups (creational → dunder → public → protected → private, each split by instance → class → static). To customise the ordering you define your own groups (below); the scalar settings tune the rest:
[tool.funcsort]
# Method type ordering within each group (secondary sort, optional)
# Options: "instance", "class", "static" Default: ["instance", "class", "static"]
method_type_order = ["instance", "class", "static"]
# Sort module-level functions too (default: true)
sort_module = true
# Exclude files/directories matching these glob patterns (optional)
# exclude = ["tests/*", "migrations/*.py"]
Custom groups (full control)
For full control, define an ordered list of [[tool.funcsort.groups]]. This replaces
the built-in groups entirely. Each group matches member names by regex (first-match-wins
down the list); the list order is the output order.
[tool.funcsort]
method_type_order = ["instance", "class", "static"]
# Sort module-level UPPER_CASE constants to the very top.
[[tool.funcsort.groups]]
name = "constants"
match = "^[A-Z][A-Z0-9_]*$"
kind = ["assignment"] # opt this group into assignments
scope = "module" # only at module scope
# Group pytest-style fixtures next, in classes only.
[[tool.funcsort.groups]]
name = "fixtures"
match = "^(setup|teardown)"
scope = "class"
# Then magic methods.
[[tool.funcsort.groups]]
name = "dunder"
match = "^__.+__$"
# Catch-all so nothing is ever "unmatched".
[[tool.funcsort.groups]]
name = "everything_else"
match = ".*"
Each group table accepts:
name(required) — identifier used in diagnostics.match(required) — a regex string or a list of strings (matched if any matches). A bare identifier (e.g."__init__") is treated as an exact-name match.kind(optional) —"function"(default) and/or"assignment". A group must opt into"assignment"for constants/assignments to be sorted; otherwise they stay anchored.type(optional) — restrict to"instance","class"and/or"static".scope(optional) — restrict to"class"and/or"module".decorator(optional) — a regex/exact string or list; the member must carry a decorator whose dotted name (calls stripped, e.g.app.routefrom@app.route("/x")) matches one.
Unmatched members: with custom groups, a member that matches no group is moved to the end of its block (preserving relative order) and reported with a warning. Add a
".*"catch-all group to collect them where you want.
Default group rules
The built-in default groups classify member names as:
- Creational: Lifecycle dunders (
__new__,__init__,__init_subclass__,__post_init__,__set_name__); to change this set, define your owncreationalgroup. - Dunder: Any other magic method (e.g.,
__str__,__repr__,__eq__,__get__) - Public: No underscore prefix (e.g.,
def method()) - Protected: Single underscore prefix (e.g.,
def _method()) - Private: Double underscore prefix, not magic (e.g.,
def __method())
Method Type Rules
- Class methods: Decorated with
@classmethod - Static methods: Decorated with
@staticmethod - Instance methods: Regular methods (no special decorator)
Sorting Behavior
Methods are sorted in two levels:
- Primary: By visibility (creational → dunder → public → protected → private by default)
- 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:
- Creational instance methods (
__init__,__new__, …) - Creational class methods
- Creational static methods
- Dunder instance methods (
__str__,__eq__, …) - Dunder class methods
- Dunder static methods
- 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
Skipping Sorting with # nosort
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 sorted
Class-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 sorted
Method-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
Usage
Command Line
# Sort a single file
funcsort example.py
# Sort multiple files
funcsort file1.py file2.py file3.py
# Sort all Python files in a directory (recursive by default)
funcsort src/
# Sort all Python files in current directory and subdirectories
funcsort .
# Non-recursive directory sorting (only files in the directory, not subdirectories)
funcsort src/ --no-recursive
# Wildcards work too (expanded by shell)
funcsort *.py
funcsort src/**/*.py
# Check if files need sorting (useful for CI)
funcsort --check example.py
funcsort --check src/
# Show diff of changes
funcsort --diff example.py
# Sort class methods only, leaving module-level functions untouched
funcsort --no-sort-module src/
# Combine flags
funcsort --check --diff src/
# Exclude specific files or directories
funcsort --exclude "tests/*" --exclude "migrations/*.py" src/
# Multiple exclude patterns (can be combined with config file patterns)
funcsort --exclude "test_*.py" --exclude "*/legacy/*" .
Note: By default, funcsort 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.
Pre-commit Integration
Add to your .pre-commit-config.yaml:
repos:
- repo: local
hooks:
- id: funcsort
name: funcsort
entry: funcsort
language: python
types: [python]
additional_dependencies: ["funcsort"]
Then install the hook:
pip install pre-commit
pre-commit install
Example
Before
class 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):
pass
After (with default config)
class 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):
pass
The methods are now organized by:
- Visibility: creational (
__init__) → dunder → public → protected → private - Type (within each visibility): instance → class → static
Development
# Install dependencies
uv sync
# Run on example file
uv run funcsort example.py
# Test with check mode
uv run funcsort --check example.py
# View diff
uv run funcsort --diff example.py
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file funcsort-0.2.0.tar.gz.
File metadata
- Download URL: funcsort-0.2.0.tar.gz
- Upload date:
- Size: 81.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9fa1af7177e640557d7ca100275c4a047bdb9acc95ac6b9d5d5d0c36c065b0c1
|
|
| MD5 |
67c5c049ef8cb1595d1092a7e05715dc
|
|
| BLAKE2b-256 |
96da72f70e12879d56514226ab93d2ea020063bfe51bf7456697a313f148dc60
|
File details
Details for the file funcsort-0.2.0-py3-none-any.whl.
File metadata
- Download URL: funcsort-0.2.0-py3-none-any.whl
- Upload date:
- Size: 19.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1bc5ddebccc52cdf4a37038e82abb038a0d8e3d71a041496e633b3b817faa265
|
|
| MD5 |
f8f9ac0b25f6fcc2b90a4c6ee0931af5
|
|
| BLAKE2b-256 |
064fb98fb5cc48e4ae49d0637f1bb969e1789ded7d8c664012d25bacbbc702d4
|