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 |
suppress-context |
with contextlib.suppress(...): — the context-manager spelling of except ...: pass |
return-none-on-missing-input |
if not path.exists(): return None — also os.path.exists/isfile/isdir, Path.is_file()/is_dir(), and any degraded return ([]/{}/0/NaN) |
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 |
Opt-in rules
Higher-false-positive rules run only when named in --enable (added on top of
the default rules) or --rules / rules= (exactly the listed rules):
| Code | What it flags |
|---|---|
return-none-on-empty-input |
if df is None: return None — also len(x) == 0, not len(x), not x, df.empty guards returning a degraded placeholder |
scilintr --enable return-none-on-empty-input src/
See ../../analysis_lint_strategy.md for
the design rationale and ../../docs/failure-modes.md
for the bugs each rule catches.
Install / use
pip install scilintr # from PyPI
# 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.
Metadata
Release files for scilintr 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| scilintr-0.1.2.tar.gz | 57.5 kB | Without provenance | Details for scilintr-0.1.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| scilintr-0.1.2-py3-none-any.whl | Python 3 | none | any | Details for scilintr-0.1.2-py3-none-any.whl |
Total release size: 113.2 kB
Release files / scilintr-0.1.2.tar.gz
| Download URL | scilintr-0.1.2.tar.gz |
|---|---|
| Size | 57.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a35ef30926b7c08f616df48016f3c1fa6b7466891cea9df00c4af34c201c8f9e
|
|
BLAKE2b-256 checksum How to use checksums |
7b0eb98d6089850744cf2c56d77c8f919ae8c50fe05f496b08f35f39d6fd76c1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.4
|
Release files / scilintr-0.1.2-py3-none-any.whl
| Download URL | scilintr-0.1.2-py3-none-any.whl |
|---|---|
| Size | 55.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
89857bbcaa81f3e90b7903e6997ee151d718e7ce0b7a80d9fd03fb159ecab369
|
|
BLAKE2b-256 checksum How to use checksums |
a7d5de8ca2268abebb2f0ec42f39279d492d8ed8aa91a5f2b488e598855d7b8a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.4
|
Security for scilintr 0.1.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.