Skip to main content

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 rule
  • test_<rule>_passes_good_code — equivalent good code produces zero findings
  • test_<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)

Source distribution for scilintr 0.1.2
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)

Table of built distributions (wheels) for scilintr 0.1.2
File Interpreter ABI Platform Actions
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.

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page