Skip to content

Ruby has no docstrings

Level: 101 · a Python programmer who reaches for help(obj) and obj.__doc__ and finds nothing of the kind in Ruby

One line: A Ruby method carries no documentation at run time — only source_location, the file and line of its def — and the comment above the def is the documentation by convention, read from the file by RDoc, YARD and ri; Python's docstring is a real object (__doc__), and inspect.getcomments does Ruby's trick too.

In Python a string literal that opens a def, class or module body is stored as __doc__; help() shows it, and every tool from IDE hovers to pydoc reads it. Ruby has nothing at run time: a comment is dropped by the parser. What a method does know is source_location — [file, line] for a method written in Ruby, nil for one implemented in C such as Integer#+ or Kernel#puts. Documentation is therefore a convention on the source text: the comment block directly above a def belongs to it, and the tools agree on that. RDoc (a bundled gem) turns those comments into HTML and into the ri database, so ri Array#each at a terminal shows the comment from Ruby's own source; YARD adds tags to the same comments — @param name [Type] description, @return [Type], @example — and generates richer pages (Not machine-checked here.). The program below is a ten-line reader that does what those tools do: take source_location, open the file, and walk up from the line above the def while the lines are comments.

Verified output of no_docstrings_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 1. Method#source_location:     [no_docstrings_rb.rb, 7] -- a String and an Integer
 2. a C-implemented method:     1.method(:+).source_location is nil, method(:puts) too: nil
 3. comment above def add:      ["Adds two numbers.", "Both must be numeric."]
 4. comment above Calc#mul:     ["the multiply helper"]
 5. no comment:                 []
 6. no Method#doc at all:       Method.instance_methods.grep(/doc/) = []
 7. a comment is not an object: the string "# Adds two numbers." exists only in the file; nothing at run time holds it
 8. RDoc parses that comment:   a Document with 2 paragraphs (parts: Paragraph, BlankLine, Paragraph)

Reading the output

Row 1 shows source_location for add: the program's own file name and the line of its def, a String and an Integer. Row 2 shows the nil you get for a method written in C — no file to read, so no comment to find, which is exactly why ri ships its own database extracted from the C sources' comments. Rows 3–5 are the reader at work: two comment lines above add, one above Calc#mul, none above undocumented. Row 6 proves the negative: Method has no doc, documentation or similar method at all. Row 7 is the underlying fact stated plainly — after parsing, the text # Adds two numbers. exists nowhere in memory. Row 8 hands the same comment to RDoc's markup parser, which sees two paragraphs; this is the first step of what rdoc and ri do with every comment in a file.

Compared with Python

Row 1 is the same information through inspect.getsourcefile and getsourcelines. Row 2 is the same C-function limit — getsourcefile(len) raises TypeError — with the difference that len.__doc__ still exists, because a C function's docstring is compiled into the interpreter. Rows 3–5 are inspect.getcomments, which reads the comment block above an object from its source file exactly as the Ruby program did, and returns None when there is none. Rows 6–8 are what Ruby lacks: sub.__doc__ is a real string, Calc.__doc__ too, and inspect.getdoc returns it with the indentation removed — use getdoc rather than raw __doc__ when you print one, because 3.13 started stripping that indentation at compile time and the raw string differs between 3.12 and 3.14 (Not machine-checked here.). help(sub) at the prompt prints the signature and this cleaned docstring, and python3 -m pydoc sub does the same from the shell. PEP 257 fixes the layout — a one-line summary, a blank line, then detail — and that is the shape getdoc(sub) shows in row 8.

Verified output of no_docstrings_py.py — regenerated by tools/run_examples.py, never hand-typed.

 1. inspect.getsourcefile/lines:  [no_docstrings_py.py, 9] -- a str and an int
 2. a C-implemented function:     getsourcefile(len) raises TypeError, yet len.__doc__ starts 'Return the number of items'
 3. comment above def add:        '# Adds two numbers.\n# Both must be numeric.\n'
 4. comment above Calc.mul:       '# the multiply helper\n'
 5. no comment, no docstring:     getcomments None, __doc__ None
 6. the real thing, __doc__:      inspect.getdoc(sub) = 'Subtract b from a.\n\nReturns a number.'
 7. a docstring is an object:     Calc.__doc__ = 'A calculator.', add.__doc__ = None (a comment is not)
 8. getdoc cleans indentation:    lines of getdoc(sub): ['Subtract b from a.', '', 'Returns a number.']

Try it

  1. Write a module Documented whose method_added(name) hook looks at the source line above each new def (via instance_method(name).source_location) and prints whether a comment is there. extend a class with it and define three methods, one of them without a comment.
  2. Extend the reader into a tiny YARD: parse @param name [Type] text and @return [Type] text lines out of the comment block above a method and print them as a table, then compare the declared parameter names with method.parameters and report any that are missing from the comment.
Solution to 1

no_docstrings_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Kata: a `Documented` hook. Extending a class with it makes `method_added`
# look at the source line above each new `def` and report whether a comment
# is there.
module Documented
  def method_added(name)
    file, line = instance_method(name).source_location
    above = File.readlines(file, chomp: true)[line - 2].to_s.strip
    puts format("%-18s %s", "#{self}##{name}:", above.start_with?("#") ? "documented (#{above})" : "NO COMMENT ABOVE")
    super
  end
end

class Calc
  extend Documented

  # Adds two numbers.
  def add(a, b) = a + b

  def sub(a, b) = a - b

  # Multiplies, the long way.
  def mul(a, b) = a * b
end

Verified output of no_docstrings_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

Calc#add:          documented (# Adds two numbers.)
Calc#sub:          NO COMMENT ABOVE
Calc#mul:          documented (# Multiplies, the long way.)

See also