Natural sorting with natsort for filenames, versions and paths: working versions (tested 2026-10-16)
Sort filenames and version strings the way humans expect with natsort, using IGNORECASE and PATH flags, parallel-list ordering and the FLOAT algorithm for decimals.
passed6 passed · 0 failed2026-10-16 21:57 UTC- Status
- passed
- Last passed
- 2026-10-16 21:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
natsort@8.4.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38972656245
The code
Exactly the files that passed. Your agent can pull them with get_recipe("natsort-natural-sorting") (install), or see them on GitHub.
requirements.txt
natsort==8.4.0 pytest==9.1.1
sorting.py
from natsort import (
index_natsorted,
natsort_keygen,
natsorted,
ns,
order_by_index,
)
def sort_filenames(names):
return natsorted(names)
def sort_versions(versions):
return natsorted(versions)
def sort_case_insensitive(values):
return natsorted(values, alg=ns.IGNORECASE)
def sort_paths(paths):
return natsorted(paths, alg=ns.PATH)
def sort_rows(filenames, sizes):
order = index_natsorted(filenames)
return [filenames[i] for i in order], order_by_index(sizes, order)
def sort_decimal_versions(versions):
key = natsort_keygen(alg=ns.FLOAT)
return sorted(versions, key=key)
tests/test_sorting.py
from natsort import natsorted
from sorting import (
sort_case_insensitive,
sort_decimal_versions,
sort_filenames,
sort_paths,
sort_rows,
sort_versions,
)
def test_filenames_use_natural_order():
names = ["file10", "file2", "file1", "file20"]
assert sorted(names) == ["file1", "file10", "file2", "file20"]
assert sort_filenames(names) == ["file1", "file2", "file10", "file20"]
def test_versions_are_compared_componentwise():
versions = ["1.9.0", "1.10.0", "1.2.3", "1.10.1"]
assert sort_versions(versions) == ["1.2.3", "1.9.0", "1.10.0", "1.10.1"]
def test_case_insensitive_needs_the_ignorecase_flag():
values = ["Apple", "apple", "Banana", "banana"]
assert natsorted(values) == ["Apple", "Banana", "apple", "banana"]
assert sort_case_insensitive(values) == ["Apple", "apple", "Banana", "banana"]
def test_path_flag_keeps_directories_together():
paths = ["a10/y", "a/10/y", "a/2/x"]
assert natsorted(paths) == ["a10/y", "a/2/x", "a/10/y"]
assert sort_paths(paths) == ["a/2/x", "a/10/y", "a10/y"]
def test_parallel_lists_follow_the_same_order():
filenames = ["img12.png", "img1.png", "img2.png"]
sizes = ["large", "small", "medium"]
ordered_files, ordered_sizes = sort_rows(filenames, sizes)
assert ordered_files == ["img1.png", "img2.png", "img12.png"]
assert ordered_sizes == ["small", "medium", "large"]
def test_float_algorithm_reads_decimals_as_numbers():
versions = ["1.5", "1.10", "1.9"]
assert sort_versions(versions) == ["1.5", "1.9", "1.10"]
assert sort_decimal_versions(versions) == ["1.10", "1.5", "1.9"]
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.