Implements the LIX (Björnsson, 1968) and RIX (Anderson, 1983) formulas for Scandinavian languages and English.
LIX is a Python library for computing readability scores (LIX, RIX, and related metrics). Open-source, minimal dependencies, typed, well-tested.
- Language: Python 3.12+
- Package manager: uv
- Linter/formatter: ruff
- Security: bandit
- Testing: pytest
- Type checking: mypy (strict mode)
uv sync # Install all dependencies (incl. dev)
uv sync --no-dev # Install production dependencies onlyuv run ruff check . # Lint (find issues)
uv run ruff check . --fix # Lint and auto-fix
uv run ruff format . # Format all files
uv run ruff format --check . # Check formatting without changing filesuv run mypy src/ # Type-check the source treeuv run bandit -r src/ # Scan for security issuesuv run pytest # Run all tests
uv run pytest tests/test_core.py # Run a single test file
uv run pytest tests/test_core.py::TestComputeLix::test_known_score # Run one test
uv run pytest -x # Stop on first failure
uv run pytest -k "lix and not slow" # Run tests matching expression
uv run pytest --cov=lix --cov-report=term-missing # With coverageuv run ruff check . && uv run ruff format --check . && uv run mypy src/ && uv run bandit -r src/ && uv run pytestLIX/
├── AGENTS.md
├── pyproject.toml # Project metadata, dependencies, tool config
├── uv.lock # Locked dependencies
├── LICENSE
├── README.md
├── src/
│ └── lix/
│ ├── __init__.py # Public API exports
│ ├── core.py # LIX/RIX score computation
│ ├── tokenizer.py # Sentence/word tokenization
│ ├── types.py # TypedDicts, dataclasses, type aliases
│ └── py.typed # PEP 561 marker
└── tests/
├── __init__.py
├── conftest.py # Shared fixtures
├── test_core.py
└── test_tokenizer.py
- Line length: 88 characters max
- Quotes: Double quotes for strings
- Trailing commas: Always on multi-line structures
- Indentation: 4 spaces, no tabs
Order enforced by ruff (I rules). Always use this grouping:
# 1. Standard library
from collections.abc import Sequence
from dataclasses import dataclass
# 2. Third-party
import pytest
# 3. Local/project
from lix.core import compute_lix
from lix.types import ReadabilityResult- Use
from __future__ import annotationsat the top of every module - Prefer
from collections.abc import Xoverfrom typing import Xfor runtime types - Never use wildcard imports (
from x import *)
| Element | Style | Example |
|---|---|---|
| Modules | snake_case | tokenizer.py |
| Classes | PascalCase | ReadabilityResult |
| Functions | snake_case | compute_lix |
| Constants | UPPER_SNAKE_CASE | LONG_WORD_THRESHOLD |
| Private members | _leading_under |
_split_sentences |
| Type aliases | PascalCase | TokenList |
- All public functions must have full type annotations (params + return)
- Private/internal functions: annotations strongly recommended
- Use
str | Noneunion syntax (notOptional[str]) - Use
X | Yunion syntax (notUnion[X, Y]) - Use built-in generics:
list[str],dict[str, int](notList,Dict) - Use
Selffromtypingfor fluent/builder patterns - Prefer
Sequence/Mappingin function parameters over concrete types
Use Google-style docstrings on all public functions and classes:
def compute_lix(text: str) -> float:
"""Compute the LIX readability score for the given text.
The LIX formula: (words / sentences) + (long_words * 100 / words)
where long words have more than 6 characters.
Args:
text: The input text to analyze. Must be non-empty.
Returns:
The LIX score as a float. Lower scores indicate easier text.
Raises:
ValueError: If text is empty or contains no sentences.
"""- Raise specific exceptions:
ValueError,TypeError— never bareException - Include actionable error messages:
raise ValueError(f"Expected non-empty text, got {len(text)} chars") - Never silently swallow exceptions (no empty
exceptorexcept: pass) - Use custom exceptions sparingly; only when callers need to distinguish error types
- Document raised exceptions in docstrings
- No magic numbers in logic — define named constants
- Constants go in the module that uses them, or in
types.pyif shared
LONG_WORD_THRESHOLD: int = 6 # Characters; words longer than this are "long words"- Use
@dataclass(frozen=True, slots=True)for value objects - Use
TypedDictfor dict shapes passed across boundaries - Avoid mutable default arguments
@dataclass(frozen=True, slots=True)
class ReadabilityResult:
lix: float
word_count: int
sentence_count: int
long_word_count: int- Test files mirror source:
src/lix/core.py→tests/test_core.py - Test function names:
test_<function>_<scenario>(e.g.,test_compute_lix_empty_text_raises) - Use
pytest.raisesfor expected exceptions - Use
pytest.approxfor float comparisons - Fixtures go in
conftest.pywhen shared across files - Parametrize over edge cases rather than writing repetitive tests
@pytest.mark.parametrize("text,expected", [
("The cat sat.", pytest.approx(5.0, abs=0.1)),
("Short.", pytest.approx(0.0, abs=0.1)),
])
def test_compute_lix_known_scores(text: str, expected: float) -> None:
assert compute_lix(text) == expected- Branch from
main; merge via PR - Commit messages: imperative mood, concise (
Add RIX score computation,Fix sentence boundary detection) - Do not commit without being explicitly asked
- Run the full CI check before committing
- Suppress type errors (
type: ignore,Anycasts) without a comment explaining why - Add runtime dependencies without discussing with the user first
- Modify the public API surface without discussing with the user first
- Write Python < 3.12 compatibility shims (we target 3.12+ only)
- Use
print()for output in library code (raise or return instead)