# Pyvista Dev

> Develop for PyVista the way the project works. Load before writing code for this repository.

- Skill: `pyvista/pyvista-dev` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pyvista/pyvista-dev`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pyvista/pyvista-dev/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: pyvista (https://skillmd.com/u/pyvista)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pyvista/pyvista-dev

---


# Developing for PyVista

Two documents already carry most of the rules, and this skill does not repeat them.
Read them, then come back here.

- `CONTRIBUTING.rst` is normative. Sections you will need:

  | Topic                                   | Section                                                    |
  | --------------------------------------- | ---------------------------------------------------------- |
  | Coding style, keyword-only arguments    | `Coding Style`                                             |
  | Standard library import rules           | `Import Conventions`                                       |
  | numpydoc rules and the sample docstring | `Docstrings`                                               |
  | Three-step deprecation policy           | `Deprecating Features or other Backwards-Breaking Changes` |
  | Image regression testing                | `Notes Regarding Image Regression Testing`                 |
  | Branch prefixes                         | `Branch Naming Conventions`                                |
  | `make` targets and tox environments     | `Quick Development Commands`                               |

- `context7.json` at the repository root holds the project's agent-facing API rules:
  wrap rather than subclass, prefer PyVista over raw VTK, filters return new datasets,
  the image-regression discipline. It is schema-validated by `pre-commit`. Treat it as the source for how to _use_ the API; this skill covers how to
  _change_ it.

## The workflow

Build here, wrap with **pyvista-vtk**, test with **pyvista-testing**, critique with
**pyvista-review**, ship with **pyvista-pr**. `AGENTS.md` at the repository root routes to
all five and carries the rules that apply before any of them.

Run the critique stage in a **subagent** and fix what it finds before opening anything.

## What the project is for

PyVista is a Pythonic interface to VTK. A feature that makes users think in VTK terms
has failed even when it works.

`CONTRIBUTING.rst` names one value that is easy to skim past and that often decides
reviews: the project wants "good code, concise accurate documentation, and avoiding
unneeded code churn". Churn is called out by name. Prefer the smaller diff.

## Before writing code

Two assumptions cause most of the rework here, and both are one `grep` from certainty:

- **that the name is free.** `cell_connectivity` exists because `connectivity` collides
  with `DataSetFilters.connectivity`. Search the class, its bases, and the filter mixins
  before choosing a public name.
- **that the constant does not already exist.** Version checks, capability probes,
  `Literal` aliases and test guards are the single most requested reuse in review.
  Version-dependent behavior in particular must use the shared constant rather than a
  locally re-derived comparison, because the local copy is what goes stale.

Where a design question has two defensible answers that lead to different code, ask
rather than picking one silently and building on it.

## Comments and docstrings

Less is more. A comment earns its place by explaining why, not what; if the code already
says what, delete the comment. Keep docstrings to what the numpydoc sections require, not
a narrative.

Every function and class gets a docstring, including small private helpers -- a one-line
summary is enough when there is nothing more to say. Only the full numpydoc sections
(`Parameters`, `Returns`, examples, ...) are reserved for the public API.

`numpydoc ignore=RT01` (no `Returns` section) is fine for a simple property getter or
setter, where the summary line already says what comes back. A function whose return
type or meaning is not obvious from the name and summary needs a real `Returns` section
instead of the ignore, however small the function is.

Never describe how the change came to be -- no "fix", "bug", "temporary workaround", "now
we", "previously", or reference to a prior version of the code. Write both as if the
current diff were the first and only version of the file: they describe the code that
exists on this branch relative to `main`, not the debugging path taken to get there.

## Size

Merged pull requests here are small: the median adds tens of lines, not hundreds. One
self-contained change per pull request, and refactors go in a separate one from features
and bug fixes. If you find an unrelated bug while working, note it and open a second pull
request rather than folding the fix in.

Large changes are sometimes correct, and a few percent of merged pull requests are large.
The recurring legitimate shapes are a cohesive new subsystem whose halves do not stand
alone, a mechanical sweep that must land atomically or the tree is inconsistent, a
deprecation that has to move every internal call site at once, and a feature whose tests
dwarf it. A change split into pieces that cannot be reviewed or merged independently is
worse than one large change.

When a change has to be large, spend the effort on the reviewer:

- Say in one line why it must land atomically.
- Keep mechanical commits separate from semantic ones. A wide rename plus a small
  behavior change is reviewable as two commits and unreadable as one.
- Name the two or three files carrying the real change and say the rest is mechanical.
- Never fold an opportunistic refactor into an already-large change.

Regenerate the size distribution with:

```bash
gh pr list -R pyvista/pyvista --state merged --limit 400 \
  --json additions,author --jq '.[] | select(.author.is_bot | not) | .additions'
```

Exclude bots, as above. Dependency bumps are a seventh of merged pull requests and pull
every percentile down.

## Conventions that are machine-enforced

`ruff` and `numpydoc` settle most style questions, so do not spend review budget on
them. The custom `pre-commit` hooks are easier to trip, and they produce a red job rather
than a comment:

| Hook                           | Rejects                                                         | Use instead                                |
| ------------------------------ | --------------------------------------------------------------- | ------------------------------------------ |
| `no-bare-import-pyvista`       | a bare `import pyvista`                                         | `import pyvista as pv`                     |
| `no-forbidden-plotter-names`   | `plotter`, `p`, `plot`, `plt`, `pltr` as the `Plotter` variable | `pl`                                       |
| `namespace-stdlib-imports`     | `import pathlib` and friends                                    | `from pathlib import Path`                 |
| `no-lint-suppression-comments` | `noqa` and `ruff:` directives under `examples/`                 | fix it, or `per-file-ignores` in pyproject |
| `no-import-error-skip`         | `importorskip`, `except ImportError`, `suppress(ImportError)`   | make the dependency available              |
| `warn_external`                | `warnings.warn` inside `pyvista/`                               | `warn_external`                            |

`warn_external` is the one that behaves differently: it is a `libcst` codemod, so it
rewrites the call for you and fails the run because the file changed. Stage its rewrite
rather than reverting it.

Scope matters when a snippet fails one of these: `namespace-stdlib-imports` and
`no-forbidden-plotter-names` run on Python, reStructuredText and Markdown;
`no-bare-import-pyvista` runs on reStructuredText and Markdown only;
`no-lint-suppression-comments` runs under `examples/` and `no-import-error-skip` under
`tests/`. A documentation snippet can fail a hook that the equivalent Python file would
pass.

Two more that `ruff` enforces and reviewers still notice: error messages are assigned to
a variable before being raised (`EM`), and boolean arguments are keyword-only
(`FBT001`/`FBT002`). Making an existing signature keyword-only goes through
`_deprecate_positional_args`, never a hard break. The counterpart of
`namespace-stdlib-imports` is ruff's `banned-from` list (`ICN003`) in `pyproject.toml`,
which forbids the opposite direction; `Import Conventions` explains why both exist.

## Editing TOML

`taplo-format` runs `reorder_keys=true` and `reorder_arrays=true`, so keys and array
entries in `pyproject.toml` and other TOML files get sorted. A comment on its own line
above an entry is attached to that entry and breaks the sort -- `taplo` will not reorder
past it. Put a comment inline, on the same line as the value it documents, so sorting
still works. Keep it short for the same reason as everywhere else: less is more.

A comment line is legitimate only when it deliberately splits the array or table into
independently-sorted sections -- `filterwarnings` in `pyproject.toml` keeps `'error'`
first with a blank line rather than a comment because pytest applies filters in order, and
the `pytest-pyvista options` comment splits the `pytest-pyvista` keys from pytest's own so
each group sorts on its own. Reach for that only when order or grouping is load-bearing,
not to leave a note.

## Tests

The heaviest review axis by a wide margin. `CONTRIBUTING.rst` and `context7.json` cover
what to write. This is what gets sent back:

- **Prove the test fails without the fix.** Revert the change, watch it go red, restore.
  It is the only reliable way to catch a test that passes for an unrelated reason.
- **Watch for incidental passes.** A uniform mesh takes a different code path than a
  mixed one, and a fixture can make an assertion true regardless of the change.
- **Test the negative case.** If a feature guards against something, make that something
  happen in a test.
- **Assert enough.** Round-trip equality rather than a single attribute.
- Parametrize rather than branching inside the test body, and do not leave a case
  commented out in a `parametrize` list.
- `filterwarnings` in `pyproject.toml` begins with `error`, so any warning that is not
  explicitly ignored fails the suite.

## Local gates

`Quick Development Commands` lists the `make` targets, each mirroring a CI job. Run
`make lint`, `make docstyle`, `make doctest`, and the test module you touched, and treat a
change as unfinished until they pass.

`make doctest` is the one that gets skipped for looking unrelated: it runs every docstring
example in the package rather than the ones in the diff, so anything that changes
import-time behavior or a plotting default can fail it without a docstring edit anywhere.
Moving `pv.BUILDING_GALLERY` out of `pyvista/ext/plot_directive.py`'s module scope did
exactly that -- collecting that module was what set the flag, and one example's
anti-aliasing warning is silenced only while a gallery is being built.

The target runs two environments: `doctest-modules` executes the examples, and
`doctest-names` statically checks that every name an example uses is bound by that example
(`tests/check_doctest_names.py`). The second executes none of the examples, so its
failures are name resolution bugs in the docstring rather than runtime errors.

`tests/conftest.py` and the `doctest-modules` tox environment already set off-screen
rendering, so the `make` targets are safe. Only a bare `pytest --doctest-modules` outside
tox needs `PYVISTA_OFF_SCREEN=true`; without it the examples open render windows and take
over the display.

**Never push a commit to find out whether something passes.** `AGENTS.md` opens with that
rule and `CONTRIBUTING.rst` states it as `Continuous Integration Etiquette`. It binds you
more tightly than a human contributor, because pushing is cheaper for you than it is for
the project. Keep the pull request in draft while you iterate.

## Approaching a red job

`Continuous Integration Etiquette` covers the basics: read the log and find the assertion,
reproduce locally, and diagnose a flaky test rather than re-running the job. Beyond that:

- Check whether the failure also occurs on `main` before attributing it to the branch. A
  second worktree at `origin/main` settles this in a minute.
- A version-specific failure needs the shared version constant, not a local workaround.
- An image regression failure is settled from the job's `failed_test_images-*` artifact,
  not by pushing a regenerated baseline to see whether it sticks. See **pyvista-testing**.

