AST-based scientific code lint for Python analysis projects — flags hidden scientific commitments.
Project description
scilintr (Python)
AST-based linter for hidden scientific commitments in Python data-analysis
code. Sibling of r/scilintr/ (R code) and
tex/scitexlintr/ (LaTeX reports); same waiver
mechanism (# ANALYSIS_OK[category]: explanation, % ANALYSIS_OK[...]:
for the TeX linter), same agent-first design philosophy (high recall
over precision).
Rules
Each rule has a kebab-case code that names the failure mode. Rules currently implemented:
| Code | What it flags |
|---|---|
broad-exception |
except Exception: without re-raise — silent error swallowing |
silent-pass |
except ...: pass (any exception type) |
silent-stub-fallback |
except ...: def f(...): return None — failure path degrades to a no-op stub (or no-op lambda rebind) |
silent-fallback-return |
except ...: return None (or []/{}/0/NaN) — failure path returns a degraded default |
silent-default-rebind |
except ...: x = {} (or []/None/0/NaN) — failure path rebinds a name to a degraded default |
return-none-on-missing-input |
if not path.exists(): return None |
positional-metadata-access |
df.iloc[:, 3] for metadata columns |
magic-threshold |
Bare numeric thresholds in DataFrame filters (padj < 0.05); > 0 / >= 0 natural floors are exempt |
unchecked-merge |
pd.merge(...) with no validate= or row-count assert |
positional-sample-alignment |
Aligning two DataFrames by row order, not key |
unannotated-filter |
df = df[df.col > X] with no length/count log |
unannotated-missingness |
df.dropna() / df.fillna() with no missingness audit |
implicit-file-selection |
glob.glob("*.csv")[0] — order-dependent file pick |
unchecked-cache |
Reading a cache file without checking when it was written |
synthetic-data-generation |
Synthetic data created with no documented purpose |
unseeded-stochastic |
np.random.* without a documented seed |
label-in-blind-stage |
Label-tainted reads in a # STAGE: selection file |
hardcoded-design-formula |
formula = "~ condition" literal in analysis code |
unannotated-transform |
np.log2(x + 1) / np.clip(...) with no rationale |
ambiguous-layer-access |
adata.X without an explicit layer= |
hardcoded-sample-ids |
Sample IDs as literals (if sample == "S123") |
warning-suppression |
warnings.filterwarnings("ignore") |
unchecked-model-fit |
.fit() returns ignored — no convergence / fit-quality check |
plot-side-effect-filter |
Plot code that mutates the DataFrame in place |
unconsumed-cli-flag |
parser.add_argument(...) whose dest is never read |
duplicate-parameter-source |
Same param declared in two places (per file) |
duplicate-parameter-source (cross-file) |
Constant in one file disagreeing with CLI default in another, scoped to a shared directory |
runtime-assert |
assert in production code (stripped by -O) |
unvalidated-config |
Config dict read without a schema check |
sentinel-mask-assignment |
df.loc[mask, col] = -999 style sentinel writes |
See ../../analysis_lint_strategy.md for
the design rationale and ../../docs/failure-modes.md
for the bugs each rule catches.
Install / use
cd py/scilintr
pip install -e ".[dev]"
# lint a file or directory
scilintr path/to/analysis/
scilintr --rules broad-exception,unchecked-merge path/to/file.py
scilintr --no-waivers path/to/file.py # audit mode: ignore ANALYSIS_OK
scilintr --summary path/to/dir/ # per-rule counts only
Exit code is 1 if any findings are produced, 0 otherwise.
API
from scilintr import lint_code, lint_paths, Finding
findings = lint_code(source_string, filename="foo.py")
findings = lint_paths(["path/to/dir/", "path/to/file.py"])
# Restrict to a subset of rules
findings = lint_code(src, rules=["broad-exception", "unchecked-merge"])
# Audit mode — surface waivered findings too
findings = lint_code(src, respect_waivers=False)
Finding is a frozen dataclass: rule, line, col, message,
severity ("hard-fail" | "structured-comment" | "warning"), filename.
Waivers
A finding can be suppressed by a structured comment on (or up to four lines before) the offending line:
# ANALYSIS_OK[api-retry]: external API requires bare Exception
try:
response = client.fetch()
except Exception:
retry()
The square-bracket category is required and free-form ([\w-]+). The
colon-and-explanation is required and must be non-empty — the structure
exists to force a reviewable statement, not to wave off arbitrary code.
Development
cd py/scilintr
pip install -e ".[dev]"
pytest # full suite
pytest tests/test_broad_exception.py # iterate on one rule
Each rule has three tests:
test_<rule>_flags_bad_code— bad code produces ≥1 finding for the ruletest_<rule>_passes_good_code— equivalent good code produces zero findingstest_<rule>_respects_waiver—# ANALYSIS_OK[<category>]:suppresses
The waiver test only becomes meaningful once the rule detects the pattern; before then it passes vacuously. That's the natural TDD inflection inside each rule: implement detection → bad-code test passes → add waiver suppression → all three pass.
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 scilintr-0.1.1.tar.gz.
File metadata
- Download URL: scilintr-0.1.1.tar.gz
- Upload date:
- Size: 47.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
275049aea0ea4f59b337ff08a3f8573b7adacc162c2aacd1ab0efd51a1186e50
|
|
| MD5 |
c8935ebf3bc38e1f0b0c10f0fc4822cd
|
|
| BLAKE2b-256 |
c6c23ea2a61660ca35c53ccd45dc66ebde18059c853b7249fae51fc3d7f61ca7
|
File details
Details for the file scilintr-0.1.1-py3-none-any.whl.
File metadata
- Download URL: scilintr-0.1.1-py3-none-any.whl
- Upload date:
- Size: 48.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a4e72314c8357dcd7763d693fee1942679d5d87adac13ec833074e84fbdeaf93
|
|
| MD5 |
522c4eaea73388aa33ef0ee1280f5459
|
|
| BLAKE2b-256 |
b7cac01d5ad26c9b5f7b0cccbdfad4a1443813479d199a722281d9292b33464c
|