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
endis indented underputs, not under itsif.-wreports 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
-wreports. 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:
-cchecks syntax and printsSyntax OK; it does not run the file. - Row 5:
class parseErroris aSyntaxError, 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?andsave!are legal method names — the convention?for a predicate and!for the dangerous version exists because the language allows the characters. - Row 7:
MAX_RETRIESis a constant andmax_retriesa 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
evalof adefwith every line at column 0 returning1: 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¶
- Write a Ruby file with three style problems — a camelCase method name, an unused variable and an
endunder the wrong column — and runruby -won it from a child process, printing each warning. Then fix the file (snake_case, no unused variable, a guard clause instead of theif) and show that the second run is clean. Note which of the three problems-wnever mentioned. - Take a method with three levels of nested
ifand 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¶
- Bang and question methods — what
?and!promise, method by method - Warnings and
-w— every warning-wcan print, captured from a child Ruby - Ruby has block comments and a data section — the magic comments of row 9 and the rest of the comment syntax
- Blocks are not objects — where
{ }anddo … enddiffer in meaning, not just style - Resources — the style guide, RuboCop and PEP 8, with links
- The Ruby Style Guide ↗ — the conventions this page measured a corner of
- PEP 8 ↗ — the Python conventions in the twin's rows 5–8
- Twenty-three Ruby traps for a Python programmer — The twenty-three places where a Python reflex gives the wrong answer in Ruby
- Twenty-three Python traps for a Ruby programmer — The twenty-three places where a Ruby reflex gives the wrong answer in Python
tapreturns the receiver,thenreturns the block's value —x.tap { … }yieldsxto the block and returnsxwhatever the block returned, so a debuggingputs…