pydocstring
A zero-dependency Rust parser for Python docstrings (Google / NumPy style).
Produces a unified syntax tree with byte-precise source locations on every token — designed as infrastructure for linters and formatters.
Python bindings are also available as pydocstring-rs.
Features
- Full syntax tree — builds a complete AST, not just extracted fields; traverse it with the built-in
Visitor+walk_tree - One code path for every style — the unified view (
Document→Section→Entry) reads any docstring with no style branching - Byte-precise source locations — every token carries its exact byte range for pinpoint diagnostics
- Zero dependencies — pure Rust, no external crates, no regex
- Error-resilient — never panics; malformed input still yields a best-effort tree
- Style auto-detection — hand it a docstring, get back
Style::Google,Style::NumPy, orStyle::Plain - Pattern matching & rewriting — find and
replaceconstructs with$NAME/$$$NAMEpatterns, substituting byte-exact captured content so everything you don't rewrite is preserved
Installation
[]
= "0.4.1"
Usage
Parsing
Parse with auto-detection and traverse the style-independent views — one code path for every docstring style:
use SectionKind;
use ;
let input = "Summary.\n\nArgs:\n x (int): The value.\n y (int): Another value.";
let parsed = parse;
let doc = new;
println!;
for section in doc.sections
When you know the style (or want to force it), use the explicit parsers —
parse_google / parse_numpy / parse_plain. They all produce the same tree,
read through the same views.
The Raw CST
The unified view is a semantic lens: it answers "is there a type?" and folds
away punctuation and the parser's zero-length placeholders. For the tree exactly
as parsed, go down to the CST with syntax():
use ;
use SyntaxKind;
let parsed = parse;
let entry = new.sections.next.unwrap.entries.next.unwrap;
// The semantic lens says "no type" …
assert!;
// … the CST says *why*: an empty type between brackets, whose zero-length range
// is the anchor to write one at. (`x:` — no brackets — has no placeholder.)
let placeholder = entry.syntax.find_missing.unwrap;
assert!;
Style Auto-Detection
use ;
assert_eq!;
assert_eq!;
assert_eq!;
Style::Plain covers docstrings with no recognised section markers: summary-only,
summary + extended summary, and unrecognised styles such as Sphinx.
Unified Auto-Detecting Parser
Use parse() to let the library detect the style and parse in one step:
use ;
use SyntaxKind;
let result = parse;
assert_eq!;
assert_eq!;
let result = parse;
assert_eq!;
Source Locations
Every view and token carries byte offsets for precise diagnostics — and they double as edit anchors:
use SectionKind;
use ;
let parsed = parse;
let doc = new;
for section in doc.sections.filter
Syntax Tree
The parse result is a tree of SyntaxNode (branches) and SyntaxToken (leaves), each tagged with a SyntaxKind. Use pretty_print() to visualize:
use parse_google;
let result = parse_google;
println!;
DOCUMENT@0..42 {
SUMMARY: "Summary."@0..8
SECTION@10..42 {
SECTION_HEADER@10..15 {
NAME: "Args"@10..14
COLON: ":"@14..15
}
ENTRY@20..42 {
NAME: "x"@20..21
OPEN_BRACKET: "("@22..23
TYPE: "int"@23..26
CLOSE_BRACKET: ")"@26..27
COLON: ":"@27..28
DESCRIPTION: "The value."@29..39
}
}
}
Visitor Pattern
Walk the tree with the Visitor trait for style-agnostic analysis:
use ;
use parse_google;
let result = parse_google;
let mut collector = NameCollector ;
walk_tree;
assert_eq!;
Style-Independent Model (IR)
Convert any parsed docstring into a style-independent intermediate representation for analysis or transformation:
use SectionKind;
use parse;
// `to_model()` dispatches on the parsed style, so this reads a NumPy
// docstring just as well.
let doc = parse.to_model;
assert_eq!;
for section in &doc.sections
Emitting (Code Generation)
Re-emit a Docstring model in any style — useful for style conversion or formatting.
Google, NumPy, and Sphinx (reStructuredText) output are supported:
use ;
use EmitOptions;
use emit_google;
use emit_numpy;
use emit_sphinx;
let doc = Docstring ;
let options = default;
let google = emit_google;
assert!;
let numpy = emit_numpy;
assert!;
let sphinx = emit_sphinx;
assert!;
assert!;
// Indent the output for embedding in a Python file:
let indented = emit_google;
assert!;
Note: Sphinx support is emit-only.
detect_stylereports Sphinx docstrings asStyle::Plain, so parsing them yields a summary/extended-summary only.
Combine parsing and emitting to convert between styles:
use parse_google;
use EmitOptions;
use emit_numpy;
let doc = parse_google.to_model;
let numpy_text = emit_numpy;
assert!;
Pattern Matching & Rewriting
Find constructs with a $NAME / $$$NAME pattern and rewrite them with a
template. Captured metavariables substitute the original source bytes, so
everything you don't explicitly rewrite is preserved byte-for-byte — the #26 use
case of annotating one entry without reflowing the rest:
use ;
use Pattern;
let src = "Summary.\n\nArgs:\n x (int): The value.\n y (str): Kept.\n";
let parsed = parse;
let pattern = new.unwrap;
let out = parsed.replace.unwrap;
assert_eq!;
Python has the same on Parsed — parsed.replace(pattern, template) and
parsed.findall(pattern):
=
=
Supported Sections
Both styles support the same section categories. A section's role is read from
Section::kind() — Args: (Google) and Parameters (NumPy) both resolve to
SectionKind::Parameters — and every entry, whatever its role, is an Entry.
| Category | SectionKind |
Read with |
|---|---|---|
| Parameters | Parameters, KeywordParameters, OtherParameters, Receives |
section.entries() |
| Returns / Yields | Returns, Yields |
section.entries() |
| Raises / Warns | Raises, Warns |
section.entries() |
| See Also | SeeAlso |
section.entries() |
| Attributes / Methods | Attributes, Methods |
section.entries() |
| References | References |
section.citations() |
| Free text (Notes, Examples, …) | FreeText(_) |
section.body() |
Document-level: summary(), extended_summary(), directives() (a deprecation
notice is a directive named deprecated), and paragraphs().
Development
Common tasks are wrapped in a justfile — run just to list them:
Or invoke cargo directly: