Skip to content
View as Markdown

Adopt LaNorme on an existing codebase

This tutorial takes a small project with existing findings to a green strict gate. You will run LaNorme, record the debt as a baseline, and end with a build that passes while holding every new line to the strict profile.

The payoff: from the first commit, only new code has to meet the standard. The debt you started with stays recorded and quiet until you choose to pay it down.

You will need Python 3.13 or newer and a terminal.

The shape of the adoption flow:

flowchart TD
    A[lanorme check .] --> B[Finds existing debt]
    B --> C[extends = strict]
    C --> D[lanorme baseline write]
    D --> E[Commit baseline like a lockfile]
    E --> F[Gate is green: debt recorded, not waived]
    F --> G{New finding?}
    G -- yes --> H[Fails the gate]
    G -- no --> I[Stays green]

What you will build

A small project with a few findings already in it. By the end:

  • [tool.lanorme] extends the strict profile.
  • A committed lanorme-baseline.json records the starting debt.
  • lanorme check . exits clean.
  • A freshly introduced violation reports, while the recorded debt stays silent.

Step 1: install

Install LaNorme into the environment you run checks from.

pip install lanorme

Confirm the command resolves:

lanorme --version

It prints lanorme followed by the installed version.

Step 2: create a project to work on

So the steps are reproducible, build a throwaway project rather than pointing at your own repository on the first read. Everything below happens inside one scratch directory.

cd "$(mktemp -d)"
git init -q
mkdir myapp

Write a module with a few ordinary problems in it: a leftover commented-out line and a function that takes too many positional parameters.

# myapp/users.py
import os


def get_user(id):
    # old = lookup(id)
    return {"id": id}


def fetch_user(id):
    return {"id": id}


def process(a, b, c, d, e, f):
    if a:
        if b:
            if c:
                if d:
                    return a + b + c + d + e + f
    return None

Add a minimal pyproject.toml so the project has a config home:

# pyproject.toml
[project]
name = "myapp"
version = "0.1.0"

Step 3: run the first check

Run LaNorme against the project. Expect findings; that is the point.

lanorme check .
[WARN] file_limits
  VIOLATION: myapp/users.py:13 — Function 'process' has parameter count 6 (warn: 5)
    Rule: PARAM-001: Function approaching the parameter limit
    Fix: Consider grouping related parameters into a dataclass or TypedDict
--- file_limits: 0 violations, 1 warnings ---

[FAIL] comments
  VIOLATION: myapp/users.py:5 — Commented-out code: old = lookup(id)
    Rule: CMT-001
    Fix: Delete it; version control remembers
--- comments: 1 violations, 0 warnings ---

Summary: 30 checks — 28 passed, 1 warnings, 1 failed.

The exit code is 1 because a violation was reported; the PARAM-001 warning on its own would have left it at 0. The default concise format shows only checks that found something, plus a summary. Other formats (full, json, ndjson, github) are available through --output-format; see the CLI reference.

Note

Exit codes are stable: 0 clean (warnings alone still exit 0), 1 violations, 2 a usage or config error. A CI step can branch on them directly.

Step 4: turn on the strict profile

extends adopts a bundled profile. The strict profile enables the opt-in checks and escalates advisory warnings to build-failing errors, so the gate is as demanding as it gets from day one. Add it to [tool.lanorme]:

# pyproject.toml
[project]
name = "myapp"
version = "0.1.0"

[tool.lanorme]
extends = ["strict"]

Run the check again:

lanorme check .
[FAIL] file_limits
  VIOLATION: myapp/users.py:13 — Function 'process' has parameter count 6 (warn: 5)
    Rule: PARAM-001: Function approaching the parameter limit
    Fix: Consider grouping related parameters into a dataclass or TypedDict
--- file_limits: 1 violations, 0 warnings ---

[FAIL] comments
  VIOLATION: myapp/users.py:5 — Commented-out code: old = lookup(id)
    Rule: CMT-001
    Fix: Delete it; version control remembers
--- comments: 1 violations, 0 warnings ---

[FAIL] docstrings
  VIOLATION: myapp/users.py:13 — Function 'process' has no docstring
    Rule: CMT-006: Public definitions past the size floor need a docstring
    Fix: Say what it is for, or what a caller needs to know that the signature does not show
--- docstrings: 1 violations, 0 warnings ---

[FAIL] named_args
  VIOLATION: myapp/users.py:13 — Function 'process' has 6 positional params without bare *
    Rule: KWARG-001: Functions with >1 parameter must use bare * separator
    Fix: Add a bare * separator: def foo(self, *, param1: str, param2: int)
--- named_args: 1 violations, 0 warnings ---

Summary: 30 checks — 26 passed, 0 warnings, 4 failed.

Four failures now. The PARAM-001 warning has become an error, and two opt-in checks have switched on and found the same function: CMT-006 for its missing docstring and KWARG-001 for its positional parameters. On a real codebase the count is larger, and fixing every finding before the gate can go green is rarely practical. The baseline is the way around that.

Bundled profiles are strict, hexagonal, clean, and layered. You can also point extends at a path to a local .toml. They merge left to right, and your own keys merge on top, so local settings always win. See extends.

Step 5: record the debt as a baseline

lanorme baseline write scans the project and records every current finding to a file. Anything in that file is suppressed on later runs, so only findings that are not already recorded report.

lanorme baseline write
Wrote 4 baseline entries (4 findings): +4 new, -0 pruned (was 0).

Add this to your configuration and commit the file like a lockfile:

    [tool.lanorme]
    baseline = "lanorme-baseline.json"

The command writes lanorme-baseline.json and prints the exact config block to add. The file is a JSON object with a version and an entries list. Each entry is keyed by file, rule code and an anchor, a hash of the source line at the finding rather than a line number, with a count of how many times it occurred, so it survives unrelated edits above it.

Step 6: point the config at the baseline

Add the printed key so checks read the baseline:

# pyproject.toml
[project]
name = "myapp"
version = "0.1.0"

[tool.lanorme]
extends = ["strict"]
baseline = "lanorme-baseline.json"

Step 7: commit the baseline like a lockfile

Commit lanorme-baseline.json alongside the config change. It is a shared, reviewable record of the debt every contributor inherits, the same way a lock file pins dependencies. Keep it in version control and review changes to it.

git add pyproject.toml lanorme-baseline.json
git commit -m "Adopt LaNorme strict profile with a debt baseline"

Step 8: confirm the gate is green

Run the check once more. The recorded findings are suppressed, so the project passes:

lanorme check .
All 30 checks passed.
Exit code: 0

The gate is green and strict at the same time. The starting debt did not move; it is recorded, not waived.

Step 9: see a new violation report

The point of the baseline is that new problems still fail the gate. Add a second module with a fresh commented-out line:

# myapp/orders.py
def place_order(cart):
    # total = compute(cart)
    return {"ok": True}
lanorme check .
[FAIL] comments
  VIOLATION: myapp/orders.py:2 — Commented-out code: total = compute(cart)
    Rule: CMT-001
    Fix: Delete it; version control remembers
--- comments: 1 violations, 0 warnings ---

Summary: 30 checks — 29 passed, 0 warnings, 1 failed.

Only the new finding in myapp/orders.py reports. The recorded debt in myapp/users.py stays quiet. New code is held to the full strict profile; old code is not blocking the build. Delete the commented-out line and the check goes green again.

Step 10: inspect the whole debt and stale entries

Two commands keep the baseline honest.

To see everything the baseline is suppressing, including the recorded debt, run a check that ignores the baseline for that run:

lanorme check --no-baseline .
[FAIL] file_limits
  VIOLATION: myapp/users.py:13 — Function 'process' has parameter count 6 (warn: 5)
    Rule: PARAM-001: Function approaching the parameter limit
    Fix: Consider grouping related parameters into a dataclass or TypedDict
--- file_limits: 1 violations, 0 warnings ---

[FAIL] comments
  VIOLATION: myapp/orders.py:2 — Commented-out code: total = compute(cart)
    Rule: CMT-001
    Fix: Delete it; version control remembers
  VIOLATION: myapp/users.py:5 — Commented-out code: old = lookup(id)
    Rule: CMT-001
    Fix: Delete it; version control remembers
--- comments: 2 violations, 0 warnings ---

[FAIL] docstrings
  VIOLATION: myapp/users.py:13 — Function 'process' has no docstring
    Rule: CMT-006: Public definitions past the size floor need a docstring
    Fix: Say what it is for, or what a caller needs to know that the signature does not show
--- docstrings: 1 violations, 0 warnings ---

[FAIL] named_args
  VIOLATION: myapp/users.py:13 — Function 'process' has 6 positional params without bare *
    Rule: KWARG-001: Functions with >1 parameter must use bare * separator
    Fix: Add a bare * separator: def foo(self, *, param1: str, param2: int)
--- named_args: 1 violations, 0 warnings ---

Summary: 30 checks — 26 passed, 0 warnings, 4 failed.

That is the full picture: the new finding plus the four recorded ones. It is your backlog whenever you want to chip away at the debt.

As you pay debt down, recorded entries that no longer match anything become stale. lanorme baseline status lists them. Pay one item down first by removing the commented-out line in myapp/users.py, then ask for status:

lanorme baseline status
1 stale baseline entry (matched nothing this run):
  myapp/users.py  CMT-001

Run 'lanorme baseline write' to prune them.

status never edits the file; it only reports. When you are happy to lock in the progress, run lanorme baseline write again to prune the stale entries and record the smaller debt. Over time the baseline shrinks toward empty, and the strict gate covers the whole codebase.

What you learned

  • extends = ["strict"] turns on the full profile, opt-in checks and all.
  • lanorme baseline write records the existing debt and prints the config block to add.
  • baseline = "lanorme-baseline.json", committed like a lock file, suppresses that debt so the gate passes.
  • New violations still report; recorded debt stays quiet until you pay it down.
  • lanorme check --no-baseline shows the whole debt; lanorme baseline status lists stale entries ready to prune.

From here, every pull request is held to the strict profile on its new code, with no upfront cleanup required.

Next steps

  • Configuration reference: every [tool.lanorme] key, including promote, per-file-ignores, and source_root.
  • Rule reference: what each check enforces and how to configure it.
  • lanorme rules lists every registered rule; lanorme rule <CODE> prints the reference section for one code.