Skip to main content

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.toml or [tool.funcsort] in pyproject.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.route from @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 own creational group.
  • 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:

  1. Primary: By visibility (creational → dunder → public → protected → private by default)
  2. 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:

  1. Creational instance methods (__init__, __new__, …)
  2. Creational class methods
  3. Creational static methods
  4. Dunder instance methods (__str__, __eq__, …)
  5. Dunder class methods
  6. Dunder static methods
  7. Public instance methods
  8. Public class methods
  9. Public static methods
  10. Protected instance methods
  11. Protected class methods
  12. Protected static methods
  13. Private instance methods
  14. Private class methods
  15. 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:

  1. Visibility: creational (__init__) → dunder → public → protected → private
  2. 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

funcsort-0.2.0.tar.gz (81.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

funcsort-0.2.0-py3-none-any.whl (19.0 kB view details)

Uploaded Python 3

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

Hashes for funcsort-0.2.0.tar.gz
Algorithm Hash digest
SHA256 9fa1af7177e640557d7ca100275c4a047bdb9acc95ac6b9d5d5d0c36c065b0c1
MD5 67c5c049ef8cb1595d1092a7e05715dc
BLAKE2b-256 96da72f70e12879d56514226ab93d2ea020063bfe51bf7456697a313f148dc60

See more details on using hashes here.

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

Hashes for funcsort-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1bc5ddebccc52cdf4a37038e82abb038a0d8e3d71a041496e633b3b817faa265
MD5 f8f9ac0b25f6fcc2b90a4c6ee0931af5
BLAKE2b-256 064fb98fb5cc48e4ae49d0637f1bb969e1789ded7d8c664012d25bacbbc702d4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page