Skip to content

Indentation is convention in Ruby, syntax in Python

Level: 101 · anyone setting up an editor or a linter for the other language

One line: Ruby runs mis-indented code and ruby -w only warns when an end sits under the wrong column, while Python refuses to compile a dedent that matches nothing; Ruby's names are conventions the parser enforces in one place (a class name must be a constant), Python's are PEP 8 conventions it enforces nowhere — and only Ruby lets a name end in ? or !.

Ruby's style lives in the community guide and in RuboCop, not in the language: two-space indentation, snake_case methods and variables, CamelCase classes and modules, SCREAMING_SNAKE_CASE constants, ? for predicates and ! for the more dangerous of two versions, { } for a one-line block and do … end for a multi-line one, guard clauses over nested ifs, and # frozen_string_literal: true at the top of a file. The language checks a little of it. ruby -w warns about a mismatched end and an unused variable but says nothing about a tab beside a space; ruby -c checks syntax alone; the parser rejects a lower-case class name because a class name is a constant; and the interpreter runs a method whose every line starts at column 0. The program starts child Rubies on small files for the warnings, then measures the naming rules and the two block styles in-process. (RuboCop and the style guide's other rules are not machine-checked here.)

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

 1. ruby -w mismatch.rb (end under the wrong column) stdout "ran anyway", exit 0
       stderr: mismatch.rb:4: warning: mismatched indentations at 'end' with 'if' at 2
 2. ruby -w unused.rb (assigned, never read)       stdout "ok", exit 0
       stderr: unused.rb:1: warning: assigned but unused variable - unused
 3. ruby -w tabs.rb (a tab line, a space line)     stdout "1", stderr "", exit 0
 4. ruby -c unused.rb (syntax check only)          stdout "Syntax OK", exit 0
 5. class parseError; end                          SyntaxError (a class name must be a constant)
 6. def empty_bag? / def save!  (predicate and bang names) true / :saved
 7. defined?(MAX_RETRIES) / defined?(max_retries)  constant / local-variable
 8. { } on one line / do...end over lines          [2, 4, 6] / [2, 4, 6]  (same result)
 9. # frozen_string_literal: true / without it     true / false
10. guard clauses: describe(nil) / describe("ann") / describe("jonathan")
       "nobody" / "hello, ann" / "too long"
11. mis-indented code still runs: eval of a def with every line at column 0 -> 1

Reading the output

  • Row 1: the file's end is indented under puts, not under its if. -w reports the mismatch on stderr, names the two keywords and their columns — and the program ran anyway (stdout "ran anyway", exit 0).
  • Row 2: an assigned-but-unused variable is the other thing -w reports. Without -w, both files are silent.
  • Row 3: a tab-indented line next to a space-indented one draws no warning at all, because Ruby's parser gives whitespace no meaning beyond the column of end.
  • Row 4: -c checks syntax and prints Syntax OK; it does not run the file.
  • Row 5: class parseError is a SyntaxError, because a class name is a constant and a constant starts with a capital letter. This is the one naming rule the parser enforces.
  • Row 6: empty_bag? and save! are legal method names — the convention ? for a predicate and ! for the dangerous version exists because the language allows the characters.
  • Row 7: MAX_RETRIES is a constant and max_retries a local, told apart by their first letter alone.
  • Row 8: the two block syntaxes give the same result; which one to use is convention (one line versus several) — the precedence difference between them belongs to the blocks chapter.
  • Row 9: the magic comment makes the file's string literals frozen; without it they are not.
  • Rows 10–11: guard clauses returning early, and eval of a def with every line at column 0 returning 1: the indentation carried no meaning.

Compared with Python

Python's indentation is the block structure, so rows 1 and 11 flip: a dedent that matches no enclosing block is an IndentationError at compile time and the file never runs, and exec of the flat def raises before executing anything. Row 3 is the case Python cares about most — a tab line beside a space line is a TabError, and the standard library ships python3 -m tabnanny to find it in a file. Row 2 shows the gap in the other direction: Python has no -w; an unused variable draws nothing from the interpreter, and pyflakes, pylint and ruff are third-party tools. (Not machine-checked here.) Rows 5–7 are PEP 8: CapWords classes, lower_case functions, UPPER_CASE constants, is_ predicates — and the interpreter enforces none of them. class parseError compiles, MAX_RETRIES rebinds even under typing.Final (which exists for type checkers), and a ? in a name is simply a SyntaxError rather than a convention. Row 8 has one block syntax, indentation, with the comprehension as the one-line form. Row 9 has no frozen-literal comment because there is nothing to freeze: every str is immutable.

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

 1. python3 mismatch.py (a dedent that matches nothing) stdout '', exit 1
       stderr: IndentationError: unindent does not match any outer indentation level
 2. python3 unused.py (assigned, never read)       stdout 'ok', stderr '', exit 0  (no lint in the stdlib)
 3. python3 -m tabnanny tabs.py (a tab line, a space line) stdout "tabs.py 3 '        y = 2'", exit 0
       running it instead: exit 1, stderr: TabError: inconsistent use of tabs and spaces in indentation
 4. python3 -m py_compile unused.py (syntax check only) stdout '', exit 0
 5. class parseError: pass                         compiled (PEP 8 says CapWords; nothing enforces it)
 6. def empty_bag?()  (a ? in a name)              SyntaxError (PEP 8 spells it is_empty)
 7. MAX_RETRIES: Final = 3; MAX_RETRIES = 4 ->     4  (UPPER_CASE is a convention; Final is for type checkers)
 8. comprehension on one line / for block over lines [2, 4, 6] / [2, 4, 6]  (same result)
 9. 'lit'[0] = 'L'   (every str is immutable)      TypeError  (no magic comment needed or possible)
10. guard clauses: describe(None) / describe("ann") / describe("jonathan")
       'nobody' / 'hello, ann' / 'too long'
11. mis-indented code cannot run: exec of a def with every line at column 0 -> IndentationError

PEP 8 asks for four spaces per level and is enforced by black and ruff the way the Ruby style guide is enforced by RuboCop; both formatters are third-party. (Not machine-checked here.)

Try it

  1. Write a Ruby file with three style problems — a camelCase method name, an unused variable and an end under the wrong column — and run ruby -w on it from a child process, printing each warning. Then fix the file (snake_case, no unused variable, a guard clause instead of the if) and show that the second run is clean. Note which of the three problems -w never mentioned.
  2. Take a method with three levels of nested if and rewrite it with guard clauses. Call both versions with the same six inputs and print a table proving they agree.
Solution to 1

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

# Kata: three style problems, reported by `ruby -w`, then fixed and re-run.
require "open3"
require "rbconfig"
require "tmpdir"

BAD = <<~RUBY
  def greetUser(name)
    unused = 1
    if name
      puts "hello, \#{name}"
      end
  end
  greetUser("ann")
RUBY

GOOD = <<~RUBY
  def greet_user(name)
    return unless name
    puts "hello, \#{name}"
  end
  greet_user("ann")
RUBY

def report(label, source)
  Dir.mktmpdir do |dir|
    Dir.chdir(dir) do
      File.write("#{label}.rb", source)
      out, err, _status = Open3.capture3(RbConfig.ruby, "-w", "#{label}.rb")
      puts "#{label}.rb under ruby -w:"
      puts "  stdout: #{out.chomp.inspect}"
      warnings = err.lines.map(&:chomp)
      puts warnings.empty? ? "  no warnings" : warnings.map { |w| "  #{w}" }
    end
  end
end

report("before", BAD)
puts "  (greetUser is legal Ruby: the camelCase method name draws no warning; only a style tool would object)"
report("after", GOOD)

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

before.rb under ruby -w:
  stdout: "hello, ann"
  before.rb:5: warning: mismatched indentations at 'end' with 'if' at 3
  before.rb:2: warning: assigned but unused variable - unused
  (greetUser is legal Ruby: the camelCase method name draws no warning; only a style tool would object)
after.rb under ruby -w:
  stdout: "hello, ann"
  no warnings

See also