Run before pushing — the ungated Lint workflow enforces these on every PR
push, and its Lint Gate job is a required status check:
ruff check diff_diff tests
black diff_diff tests
mypy diff_diff- Tool versions are pinned exactly in the
devextra ofpyproject.toml(black,ruff,mypy) and mirrored in.github/workflows/lint.yml(all three; the sync is enforced byTestLintWorkflowPinSync).mypy diff_diffis enforced at zero errors by the Lint workflow's Mypy job. A version bump is a deliberate PR updating both surfaces together. Refresh withpip install -e ".[dev]". The pinned tools require Python >= 3.10 (dev tooling only; the library itself still supports Python 3.9). [tool.ruff.lint.per-file-ignores]entries are deliberate (trop logger-before-imports E402, honest_did math-notationlE741,__init__re-export F401, conftest import ordering E402). Do not "fix" those patterns or remove the ignores.- The 2026-07 repo-wide normalization commits are listed in
.git-blame-ignore-revs; rungit config blame.ignoreRevsFile .git-blame-ignore-revsonce sogit blameattributes lines to their real authors. - Branches created before the normalization commits: rebase onto main, then
run
ruff check --fix+blackon your touched files before pushing.
When implementing new functionality, always include accompanying documentation updates.
README.md is the GitHub/PyPI first-impression surface. Keep it lean (~190 lines). Most new content does NOT belong here.
Only edit README.md when:
- A new estimator is added (one line in the
## Estimatorsflat catalog) - A new top-level capability lands (one paragraph in
## Diagnostics & Sensitivityor## Survey Support) - Hero image, badges, or top-of-fold value-prop changes
- Documentation links rot
If you find yourself adding a usage example, a parameter table, or a multi-paragraph explanation to the README, you are in the wrong file - those belong on RTD or in diff_diff/guides/llms.txt.
-
diff_diff/guides/llms.txt(AI-agent source of truth) - Add:- One-line catalog entry in the
## Estimatorssection with paper citation + RTD link - One-line entry in
## Diagnostics and Sensitivity Analysisif applicable - This file is published on RTD via
docs/conf.pyhtml_extra_pathand bundled in the wheel viaget_llm_guide()- it is the canonical machine-readable contract
- One-line catalog entry in the
-
docs/api/*.rst(technical source of truth) - Add:- RST documentation with
autoclassdirectives - Method summaries
- References to academic papers
- RST documentation with
-
docs/tutorials/*.ipynb- Update relevant tutorial or create new one:- Working code examples
- Explanation of when/why to use the feature
- New notebooks are registered in
docs/tutorials/index.rst(NOTdocs/index.rst): add a toctree entry with a short display label to the matching group (Business Applications / Fundamentals / Advanced Methods / Study Design) plus agrid-item-cardin that group's card grid
-
docs/references.rst(bibliography source of truth) - Add:- Full citation under the appropriate sub-section (matches the
### Subsectionheadings already in that file) - Use the RST format:
**Author (Year).** "Title." *Journal*, vol(num), pages. <https://doi.org/X>
- Full citation under the appropriate sub-section (matches the
-
README.md- Add ONLY:- One line in the
## Estimatorscatalog with the paper citation and RTD link
- One line in the
-
CHANGELOG.md- Add a release-note bullet under the next unreleased version. -
CLAUDE.md- Update only if adding new critical rules or design patterns. -
ROADMAP.md- Update only if shipping moves an item from planned to current. -
docs/doc-deps.yaml- Add source-to-doc mappings for the new module.
The documentation site's information architecture is machine-enforced; a red
docs-tests check means one of these was violated:
- The root
docs/index.rsttoctree lists ONLY the 5 section landing pages (Getting Started / Practitioner Guide / Tutorials / User Guide / API Reference). New top-level pages join a section landing page, never the root - every root entry becomes a navbar item and past 5 the theme regrows an unusable "More" dropdown. - Every tutorial notebook has BOTH a short-labeled toctree entry (<= 40 chars)
and a
grid-item-card, in the SAME group ofdocs/tutorials/index.rst. - The homepage "Supported Estimators" table stays 1:1 with the
docs/api/index.rstEstimators autosummary - new estimators need both. - A class documented with
:no-index:on a module page must keep a canonical autosummary entry indocs/api/index.rst, else:class:cross-references to it render as dead text.
- Update relevant docstrings
- Add/update tests
- Update
CHANGELOG.md - If methodology-related: Update
docs/methodology/REGISTRY.mdedge cases section - README is almost never the right place - skip it unless the bug was in a README claim
For methods based on academic papers, always include:
- Full citation in
docs/references.rstunder the appropriate### Subsectionheading (NOT in README) - Reference in RST API docs with paper details
- Citation in tutorial summary
- Optional: methodology reference in
docs/methodology/REGISTRY.mdfor non-trivial design choices
Example format (RST):
- **Sun, L., & Abraham, S. (2021).** "Estimating Dynamic Treatment Effects in Event Studies with Heterogeneous Treatment Effects." *Journal of Econometrics*, 225(2), 175-199. https://doi.org/10.1016/j.jeconom.2020.09.006
- Don't just test that code runs without exception
- Assert the expected behavior actually occurred
- Bad:
result = func(bad_input)(only tests no crash) - Good:
result = func(bad_input); assert np.isnan(result.coef)(tests behavior)
- Test parameter appears in
get_params()output - Test
set_params()modifies the attribute - Test parameter actually affects behavior (not just stored)
- Capture warnings with
warnings.catch_warnings(record=True) - Assert warning message was emitted
- Assert the warned-about behavior occurred
Use assert_nan_inference() from conftest.py to validate ALL inference fields are
NaN-consistent. Don't check individual fields separately.
Storing a parameter is the easy half. Before implementing, grep for every place it will have to be honoured:
grep -rn "self\.<param>" diff_diff/<module>.pyA new parameter is only complete when it is:
- stored on
selfand returned byget_params()(and accepted byset_params()) - applied in every aggregation mode —
simple,event_study, andgroup - applied in the bootstrap/inference paths, not just the analytical one
- reflected on the result object, so
to_dict()/summary()do not misreport it - propagated to the estimators that inherit it (see the inheritance map in
CLAUDE.md— subclasses ofDifferenceInDifferencesinherit automatically, standalone estimators must each be updated)
The recurring bug is a parameter that works in the default aggregation and is silently ignored in one of the others. The same trace applies when adding a new mode or code path: follow it through aggregation, bootstrap, and results before declaring it done.
When you add a new mode or code path (e.g. base_period="varying", a new
control-selection rule), verify the control/comparison group is composed correctly
for that path, not just the default. In staggered designs especially, confirm the
"not-yet-treated" comparison group excludes the treatment cohort itself, and that
parameter interactions (new mode × each aggregation method) select the intended
group. A silently mis-composed comparison group produces a plausible but wrong effect
with no error.
Wrap all related operations in np.errstate(), not just the one that raised.
A guard around the final division still lets an upstream matrix multiply or
subtraction emit the warning. Include division, matrix multiplication, and any
operation that can overflow or underflow.
Grep for every occurrence before fixing any of them, and fix them all in the same PR. Incremental fixes across review rounds are how a pattern bug survives: each round looks resolved, and the next reviewer finds the sibling you missed. If the same class of finding comes back a third time, stop patching sites and name the invariant instead.