PONY λ M2 Modula-2

Python.CodeCompared.To/Ruby

An interactive executable cheatsheet comparing Python and Ruby

Python 3.13 Ruby 4.0
Output & Running
Hello, World
print("Hello, World!")
puts "Hello, World!"
Both print() and puts append a newline by default. Ruby's puts is the idiomatic equivalent of Python's print(). Ruby scripts run top-to-bottom with no boilerplate class or main function required.
Output functions
message = "hello" print(message) # with newline print(message, end="") # without newline print(repr(message)) # developer representation
message = "hello" puts message # with newline print message # without newline puts # blank line p message # developer representation
Ruby's puts corresponds to Python's print(), print to print(..., end=""), and p to print(repr(...)). Note that Ruby's print (no newline) is a different method from Python's print().
Comments
# Single-line comment # Python has no built-in multi-line comment syntax. # Use consecutive # lines for block comments. x = 42 # inline comment
# Single-line comment # Ruby also has no multi-line comment syntax in common use. # Use consecutive # lines for block comments. # (=begin/=end exists but is rarely used.) x = 42 # inline comment
Both languages use # for single-line comments. Neither has a widely-used multi-line comment literal. Python has """docstrings""" used as documentation; Ruby has =begin/=end but it is rare in practice.
No main guard
# Python: code at top level runs when the file is executed directly # Use if __name__ == "__main__": to prevent running on import def greet(name): return f"Hello, {name}!" if __name__ == "__main__": print(greet("World"))
# Ruby: code at top level always runs when the file is loaded. # No import system that re-executes files — require caches the load. def greet(name) "Hello, #{name}!" end puts greet("World")
Ruby has no equivalent of Python's if __name__ == "__main__": guard. The require system caches loaded files, so you don't need to protect against re-execution. Scripts that both define reusable code and run standalone are common — just put the runnable part at the top level.
Formatted output
name = "Alice" age = 30 print(f"{name} is {age} years old") print(f"next year: {age + 1}")
name = "Alice" age = 30 puts "#{name} is #{age} years old" puts "next year: #{age + 1}"
Ruby's #{...} interpolation in double-quoted strings is directly equivalent to Python's f"{...}" f-strings. Both evaluate arbitrary expressions inside the delimiters. Ruby interpolation works in "double" strings and heredocs but not in 'single' strings.
Variables & Types
Variable assignment
count = 10 message = "hello" is_done = True nothing = None print(type(count)) # <class 'int'> print(type(message)) # <class 'str'>
count = 10 message = "hello" is_done = true nothing = nil puts count.class # Integer puts message.class # String
Variable assignment is identical — no declarations, no type annotations required. Python's None becomes Ruby's nil. Python's True/False become Ruby's true/false (lowercase). Use .class instead of type() to inspect the type.
None vs nil
value = None print(value is None) # True print(value == None) # True (use 'is None' by convention) def maybe_find(items, target): for item in items: if item == target: return item return None result = maybe_find([1, 2, 3], 9) print(result) # None
value = nil puts value.nil? # true puts value == nil # true (== is fine in Ruby) def maybe_find(items, target) items.find { |item| item == target } end result = maybe_find([1, 2, 3], 9) puts result.inspect # nil
Ruby's nil is an object (of class NilClass) — you can call methods on it. Check for nil with .nil? or == nil; both are idiomatic (unlike Python where is None is preferred over == None). Ruby's Enumerable#find returns nil automatically when nothing matches.
Truthiness
# Python: False, None, 0, 0.0, "", [], {}, set() are all falsy values = [False, None, 0, "", [], {}] for value in values: if not value: print(f"{repr(value)} is falsy")
# Ruby: ONLY false and nil are falsy — everything else is truthy values = [false, nil, 0, "", [], {}] values.each do |value| unless value puts "#{value.inspect} is falsy" end end
This is one of the most important differences. In Python, 0, "", [], and {} are all falsy. In Ruby, only false and nil are falsy0 is truthy, "" is truthy, [] is truthy. This trips up nearly every Python developer coming to Ruby.
Type checking
number = 42 text = "hello" print(type(number)) # <class 'int'> print(isinstance(number, int)) # True print(isinstance(text, (str, int))) # True
number = 42 text = "hello" puts number.class # Integer puts number.is_a?(Integer) # true puts text.is_a?(String) # true puts text.is_a?(Numeric) # false
Use .class instead of type() and .is_a? instead of isinstance(). Ruby's integer type is Integer (not int). is_a? respects the inheritance hierarchy, so 42.is_a?(Numeric) is true even though 42.class is Integer.
Multiple assignment
# Tuple unpacking first, second, third = 1, 2, 3 print(first, second, third) # 1 2 3 # Splat in unpacking head, *tail = [10, 20, 30, 40] print(head) # 10 print(tail) # [20, 30, 40] # Swap a, b = 1, 2 a, b = b, a print(a, b) # 2 1
# Parallel assignment first, second, third = 1, 2, 3 puts "#{first} #{second} #{third}" # 1 2 3 # Splat in assignment head, *tail = [10, 20, 30, 40] puts head # 10 puts tail.inspect # [20, 30, 40] # Swap a, b = 1, 2 a, b = b, a puts "#{a} #{b}" # 2 1
Ruby's parallel assignment mirrors Python's tuple unpacking almost exactly. The splat operator * collects remaining values just like Python's * in unpacking. Swapping variables with a, b = b, a works identically in both languages.
Constants
# Python: constants are a convention (UPPER_SNAKE_CASE) # The interpreter does not enforce immutability MAX_SIZE = 100 PI = 3.14159 MAX_SIZE = 200 # No error — just bad practice print(MAX_SIZE)
# Ruby: constants are enforced — reassignment produces a warning MAX_SIZE = 100 PI = 3.14159 # MAX_SIZE = 200 # Warning: already initialized constant MAX_SIZE puts MAX_SIZE puts PI
Ruby enforces constant naming: any identifier starting with a capital letter is a constant, and reassigning it produces a warning. Python's ALL_CAPS convention for constants is purely social — the language does nothing to enforce it. Class and module names in Ruby are also constants (String, Array, etc.).
Strings
Single vs double quotes
name = "Alice" greeting = 'Hello' # identical to double quotes in Python # Both support escape sequences tab_demo = "col1\tcol2" newline = 'line1\nline2' print(tab_demo) print(newline)
name = "Alice" greeting = 'Hello' # single quotes do NOT interpolate # Single quotes: only \\ and \' are escape sequences tab_demo = "col1 col2" # double: escape sequences work literal = 'col1 col2' # single: is literal backslash-t puts tab_demo puts literal
In Python, single and double quotes are completely interchangeable. In Ruby, single-quoted strings are literal — they do not interpolate #{} and only recognize \\ and \' as escapes. Double-quoted strings support both interpolation and the full set of escape sequences. Use double quotes when you need interpolation or escapes; single quotes for simple string literals.
f-strings vs interpolation
name = "Alice" age = 30 # f-strings (Python 3.6+) print(f"Name: {name}, Age: {age}") print(f"In 5 years: {age + 5}") print(f"Upper: {name.upper()}")
name = "Alice" age = 30 # String interpolation (double-quoted strings) puts "Name: #{name}, Age: #{age}" puts "In 5 years: #{age + 5}" puts "Upper: #{name.upcase}"
Ruby's #{...} interpolation predates Python's f-strings and works identically — any expression is valid inside the braces. Note that Ruby uses .upcase (no parentheses needed for no-argument methods) rather than Python's .upper().
Multiline strings
# Triple-quoted strings poem = """ Roses are red, Violets are blue, Python is cool, And Ruby is too. """ print(poem.strip())
# Heredoc (squiggly heredoc strips leading whitespace) poem = <<~POEM Roses are red, Violets are blue, Python is cool, And Ruby is too. POEM puts poem.chomp
Ruby's squiggly heredoc (<<~IDENTIFIER) strips leading whitespace proportionally — similar to Python's textwrap.dedent(). The terminating identifier must be at the start of its own line. Ruby also supports plain < (preserves all whitespace) and interpolating heredocs with double-quoted delimiters.
Common string methods
text = " Hello, World! " print(text.strip()) # "Hello, World!" print(text.upper()) # " HELLO, WORLD! " print(text.lower()) # " hello, world! " print(text.replace(",", ";")) print("hello world".split()) # ['hello', 'world'] print(len("hello")) # 5
text = " Hello, World! " puts text.strip # "Hello, World!" puts text.upcase # " HELLO, WORLD! " puts text.downcase # " hello, world! " puts text.gsub(",", ";") puts "hello world".split.inspect # ["hello", "world"] puts "hello".length # 5
Ruby uses .upcase/.downcase instead of .upper()/.lower(), .gsub instead of .replace(), and .length or .size instead of len(). Methods that take no arguments are called without parentheses by convention. The ! suffix (.strip!, .upcase!) mutates the string in place.
String formatting
# % operator (classic) print("Hello, %s! You are %d years old." % ("Alice", 30)) # format() method print("{name} scored {score:.2f}".format(name="Bob", score=95.678)) # f-string (modern, preferred) score = 95.678 print(f"Score: {score:.2f}")
# % operator (similar to Python's % / C's sprintf) puts "Hello, %s! You are %d years old." % ["Alice", 30] # sprintf / format method puts format("%.2f", 95.678) puts sprintf("%s scored %.2f", "Bob", 95.678) # Interpolation (modern, preferred) score = 95.678 puts "Score: #{"%.2f" % score}"
Ruby supports the % operator for sprintf-style formatting, passing an array as the right operand. The format and sprintf methods work like Python's format(). For most cases, string interpolation (#{"%.2f" % value}) is idiomatic. Ruby does not have a named-placeholder equivalent of Python's .format(name=value).
Frozen (immutable) strings
# Python strings are always immutable text = "hello" # text[0] = "H" # TypeError: 'str' object does not support item assignment # To "modify" a string, create a new one text = text.capitalize() print(text) # Hello
# Ruby 4.0: string literals are frozen by default (immutable) text = "hello" # text[0] = "H" # FrozenError: can't modify frozen String # Create a mutable copy with .dup or use .upcase (returns new string) text = text.capitalize puts text # Hello # Or build a new mutable string mutable = +"hello" # unary + creates unfrozen copy mutable[0] = "H" puts mutable # Hello
Python strings have always been immutable. Ruby strings were historically mutable, but Ruby 4.0 froze string literals by default — making Ruby's behavior match Python's. The unary + operator (+"string") creates a mutable (unfrozen) copy when you genuinely need in-place mutation.
Numbers
Integer arithmetic
print(10 + 3) # 13 print(10 - 3) # 7 print(10 * 3) # 30 print(10 ** 3) # 1000 print(10 % 3) # 1
puts 10 + 3 # 13 puts 10 - 3 # 7 puts 10 * 3 # 30 puts 10 ** 3 # 1000 puts 10 % 3 # 1
Integer arithmetic operators are identical between Python and Ruby, including ** for exponentiation and % for modulo. Both languages support arbitrarily large integers without overflow.
Integer vs floor division
# Python: / always returns float, // for integer (floor) division print(7 / 2) # 3.5 (float) print(7 // 2) # 3 (floor division) print(-7 // 2) # -4 (floors toward negative infinity)
# Ruby: / between two integers returns an integer (truncates toward zero) puts 7 / 2 # 3 (integer division) puts 7.0 / 2 # 3.5 (float when either operand is float) puts -7 / 2 # -4 (Ruby also floors toward negative infinity)
Key difference: Python's / always produces a float (7/2 == 3.5), requiring // for integer division. Ruby's / on two integers produces an integer (7/2 == 3) — make one operand a float to get a float result. Both floor toward negative infinity for negative numbers.
Arbitrary-precision integers
big = 2 ** 100 print(big) print(type(big)) # <class 'int'> factorial = 1 for i in range(1, 31): factorial *= i print(factorial) # 30!
big = 2 ** 100 puts big puts big.class # Integer factorial = (1..30).reduce(:*) puts factorial # 30!
Both Python and Ruby support arbitrarily large integers natively — no BigInteger library needed. Ruby's single Integer type handles both small and large integers automatically (Python's int works the same way). Ruby's (1..30).reduce(:*) is idiomatic for factorial.
Numeric conversions
print(int("42")) # 42 print(float("3.14")) # 3.14 print(str(99)) # "99" print(int(3.9)) # 3 (truncates, does not round) print(round(3.9)) # 4
puts "42".to_i # 42 puts "3.14".to_f # 3.14 puts 99.to_s # "99" puts 3.9.to_i # 3 (truncates, does not round) puts 3.9.round # 4
Ruby uses method calls for conversions: .to_i (to integer), .to_f (to float), .to_s (to string). Python uses constructor-style functions: int(), float(), str(). Both truncate toward zero when converting a float to integer.
Lists / Arrays
Creation and access
numbers = [10, 20, 30, 40, 50] print(numbers[0]) # 10 (first) print(numbers[-1]) # 50 (last) print(numbers[1:3]) # [20, 30] print(len(numbers)) # 5
numbers = [10, 20, 30, 40, 50] puts numbers[0] # 10 (first) puts numbers[-1] # 50 (last) puts numbers[1, 2].inspect # [20, 30] (start, length) puts numbers[1..2].inspect # [20, 30] (range) puts numbers.length # 5
Array indexing works the same — zero-based, negative indices count from the end. Slicing differs: Python uses list[start:end] (exclusive end), while Ruby uses either a Range (array[1..2] inclusive) or a start-plus-length form (array[1, 2]). Use .length or .size instead of len().
Common mutation methods
items = [1, 2, 3] items.append(4) # [1, 2, 3, 4] items.insert(0, 0) # [0, 1, 2, 3, 4] items.remove(2) # [0, 1, 3, 4] popped = items.pop() # 4 removed print(items) # [0, 1, 3]
items = [1, 2, 3] items.push(4) # [1, 2, 3, 4] (also: items << 4) items.unshift(0) # [0, 1, 2, 3, 4] items.delete(2) # [0, 1, 3, 4] popped = items.pop # 4 removed puts items.inspect # [0, 1, 3]
Python's .append() is Ruby's .push() or the shovel operator <<. Python's .insert(0, x) is Ruby's .unshift(x). Python's .remove(value) is Ruby's .delete(value). Ruby's .pop and Python's .pop() both remove and return the last element.
Higher-order operations
numbers = [3, 1, 4, 1, 5, 9, 2, 6] print(sorted(numbers)) # [1, 1, 2, 3, 4, 5, 6, 9] print(list(reversed(numbers))) # [6, 2, 9, 5, 1, 4, 1, 3] print(numbers.count(1)) # 2 print(sum(numbers)) # 31 print(min(numbers), max(numbers))
numbers = [3, 1, 4, 1, 5, 9, 2, 6] puts numbers.sort.inspect # [1, 1, 2, 3, 4, 5, 6, 9] puts numbers.reverse.inspect # [6, 2, 9, 5, 1, 4, 1, 3] puts numbers.count(1) # 2 puts numbers.sum # 31 puts "#{numbers.min} #{numbers.max}"
Python uses standalone functions (sorted(), reversed(), sum(), min(), max()) for many operations on lists. Ruby defines these as methods directly on the array: .sort, .reverse, .sum, .min, .max. Ruby's .sort returns a new array (like Python's sorted()); use .sort! to sort in place.
Array operators
a = [1, 2, 3] b = [4, 5, 6] print(a + b) # [1, 2, 3, 4, 5, 6] concatenation print(a * 3) # [1, 2, 3, 1, 2, 3, 1, 2, 3] repeat print(3 in a) # False print(4 in b) # True
a = [1, 2, 3] b = [4, 5, 6] puts (a + b).inspect # [1, 2, 3, 4, 5, 6] puts (a * 3).inspect # [1, 2, 3, 1, 2, 3, 1, 2, 3] puts a.include?(3) # true puts b.include?(4) # true
+ for concatenation and * for repetition work the same in both languages. Python's in operator becomes Ruby's .include? method. Ruby also has set-like operators: & (intersection), | (union), and - (difference) on arrays.
Dicts / Hashes
Creation and access
person = {"name": "Alice", "age": 30, "city": "NYC"} print(person["name"]) # Alice print(person.get("name")) # Alice print(person.get("country", "US")) # US (default)
person = { name: "Alice", age: 30, city: "NYC" } puts person[:name] # Alice puts person.fetch(:name) # Alice puts person.fetch(:country, "US") # US (default)
Ruby hashes commonly use symbols as keys (:name) rather than strings. The { key: value } syntax is shorthand for { :key => value }. Use .fetch(key) for stricter access (raises KeyError if missing) or .fetch(key, default) for a default — equivalent to Python's dict.get(key, default). Accessing a missing key returns nil (not a KeyError).
Iteration
scores = {"Alice": 95, "Bob": 87, "Carol": 92} for name, score in scores.items(): print(f"{name}: {score}") print(list(scores.keys())) print(list(scores.values()))
scores = { alice: 95, bob: 87, carol: 92 } scores.each do |name, score| puts "#{name}: #{score}" end puts scores.keys.inspect puts scores.values.inspect
Python's dict.items() becomes hash.each with a two-argument block. dict.keys() and dict.values() work the same in Ruby (without parentheses). Ruby iterates in insertion order, as Python 3.7+ dicts do.
Merge and transform
defaults = {"color": "red", "size": "medium"} overrides = {"size": "large", "weight": "heavy"} # Merge (Python 3.9+: | operator) merged = defaults | overrides print(merged) # Dict comprehension squared = {k: v**2 for k, v in {"a": 2, "b": 3}.items()} print(squared)
defaults = { color: "red", size: "medium" } overrides = { size: "large", weight: "heavy" } # Merge (right side wins) merged = defaults.merge(overrides) puts merged.inspect # Transform values squared = { a: 2, b: 3 }.transform_values { |value| value ** 2 } puts squared.inspect
Ruby's .merge is equivalent to Python 3.9's | operator or {**a, **b} — the right-hand hash wins on conflicts. Ruby 2.4+ has .transform_values and .transform_keys for mapping over hashes without manually building a new one.
Default values
from collections import defaultdict # defaultdict — missing key creates a default value word_count = defaultdict(int) words = ["apple", "banana", "apple", "cherry", "banana", "apple"] for word in words: word_count[word] += 1 print(dict(word_count))
# Hash.new with a default block word_count = Hash.new(0) words = ["apple", "banana", "apple", "cherry", "banana", "apple"] words.each { |word| word_count[word] += 1 } puts word_count.inspect # Or use tally (Ruby 2.7+) puts words.tally.inspect
Python's defaultdict requires an import; Ruby's equivalent is Hash.new(default_value) or Hash.new { |hash, key| hash[key] = computed_default }. Ruby 2.7 added .tally, which counts occurrences in a collection — a one-liner replacement for the word-count pattern.
Ranges
Range creation
# range() is exclusive of the end numbers = list(range(1, 11)) # 1 through 10 print(numbers) for number in range(5): print(number, end=" ")
# .. is inclusive, ... is exclusive numbers = (1..10).to_a # 1 through 10 puts numbers.inspect (0...5).each { |number| print "#{number} " } puts
Python's range(1, 11) is exclusive of 11, equivalent to Ruby's 1...11 (three dots, exclusive end). Ruby's 1..10 (two dots) is inclusive of 10. Ruby ranges are objects — they respond to methods like .each, .to_a, .include?, and .step.
Step and iteration
# range with step for number in range(0, 20, 3): print(number, end=" ") print() # Descending for number in range(10, 0, -1): print(number, end=" ")
# step method (0..19).step(3) { |number| print "#{number} " } puts # Descending 10.downto(1) { |number| print "#{number} " } puts # Or: step with negative (10).downto(1).each { |number| print "#{number} " }
Ruby's .step(n) on a range mirrors range(start, stop, step). For descending iteration, Ruby offers the expressive 10.downto(1) and 1.upto(10) methods on integers, in addition to .step(-1).
Range membership and case
score = 85 if 90 <= score <= 100: grade = "A" elif 80 <= score < 90: grade = "B" elif 70 <= score < 80: grade = "C" else: grade = "F" print(grade)
score = 85 grade = case score when 90..100 then "A" when 80..89 then "B" when 70..79 then "C" else "F" end puts grade
Ruby's case/when with ranges is a natural fit for grade-boundary logic. The range membership check (80..89 === score) is implicit. Python's chained comparison (80 <= score < 90) is readable but requires a full if/elif chain — Ruby's case/when is more concise for this pattern.
Control Flow
if / elif / else
temperature = 22 if temperature > 30: print("Hot") elif temperature > 20: print("Warm") elif temperature > 10: print("Cool") else: print("Cold")
temperature = 22 if temperature > 30 puts "Hot" elsif temperature > 20 puts "Warm" elsif temperature > 10 puts "Cool" else puts "Cold" end
Ruby uses elsif (not elif and not else if). Blocks are delimited by end, not by indentation. Parentheses around the condition are optional. Python's significant indentation forces one style; Ruby is indentation-agnostic (though 2-space indent is conventional).
unless (no Python equivalent)
# Python has no 'unless' — use 'if not' or 'if x is None' logged_in = False if not logged_in: print("Please log in") x = None if x is None: print("x is not set")
logged_in = false unless logged_in puts "Please log in" end x = nil puts "x is not set" if x.nil? puts "Please log in" unless logged_in
Ruby has unless condition as a shorthand for if !condition — it reads more naturally for negative conditions. Use it sparingly; avoid unless ... else (just flip to if). The postfix forms statement if condition and statement unless condition are idiomatic for single-line guards.
Ternary expression
age = 20 status = "adult" if age >= 18 else "minor" print(status) # Python's ternary: value_if_true if condition else value_if_false
age = 20 status = age >= 18 ? "adult" : "minor" puts status # Ruby's ternary: condition ? value_if_true : value_if_false
Python's ternary reads left-to-right: value if condition else other. Ruby uses the C-style condition ? value : other. Both are expressions that return a value. Python's form is often considered more readable for simple cases; Ruby's is more familiar to developers from C/Java/JavaScript backgrounds.
case / when
# Python 3.10+: match / case command = "quit" match command: case "quit" | "exit": print("Goodbye!") case "help": print("Available commands: quit, help") case _: print(f"Unknown command: {command}")
command = "quit" case command when "quit", "exit" puts "Goodbye!" when "help" puts "Available commands: quit, help" else puts "Unknown command: #{command}" end
Ruby's case/when predates Python's match/case (added in Python 3.10). Ruby's when accepts a comma-separated list of alternatives. Under the hood, Ruby uses === for matching — which means when can match against Regexps, Ranges, classes, and any object that implements ===.
Postfix if / unless
# Python has no postfix 'if' — always prefix x = 42 if x > 0: print("positive") items = [] if not items: print("empty list")
x = 42 puts "positive" if x > 0 items = [] puts "empty list" if items.empty? # unless is also available postfix puts "not zero" unless x.zero?
Ruby's postfix if and unless are idiomatic for single-condition guards. Python has no equivalent. The Ruby style guide recommends postfix for simple, short conditions — it reads like English: "do this if that". Avoid postfix when the condition is complex or when the statement is already long.
Loops & Iteration
for loop vs each
fruits = ["apple", "banana", "cherry"] for fruit in fruits: print(fruit)
fruits = ["apple", "banana", "cherry"] fruits.each do |fruit| puts fruit end # Shorthand for single-expression blocks fruits.each { |fruit| puts fruit }
Ruby has no for/in loop in common use — iteration is always via methods like .each, .map, .select, etc. This makes Ruby's iteration object-oriented: .each is a method call on the collection, and the block do |x| ... end or { |x| ... } is a closure passed to it.
while / until
count = 0 while count < 5: print(count) count += 1 # Python has no 'until' keyword count = 10 while count != 0: count -= 1 print("done")
count = 0 while count < 5 puts count count += 1 end # Ruby has 'until' — reads more naturally for some conditions count = 10 until count.zero? count -= 1 end puts "done"
Both use while for conditional loops. Ruby also provides until condition as the opposite of while — equivalent to while !condition. Note that Ruby lacks += and -= compound operators for some types, but they work on integers just as in Python.
break / continue vs break / next
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] for number in numbers: if number % 2 == 0: continue # skip even numbers if number > 7: break # stop at 7 print(number)
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] numbers.each do |number| next if number.even? # skip even numbers break if number > 7 # stop at 7 puts number end
Python's continue becomes Ruby's next — it skips to the next iteration. break works the same in both. In Ruby, next and break work inside any block, not just traditional loops — so they work with .each, .map, and other iterators.
enumerate() vs each_with_index
fruits = ["apple", "banana", "cherry"] for index, fruit in enumerate(fruits): print(f"{index}: {fruit}") # Custom start index for index, fruit in enumerate(fruits, start=1): print(f"{index}: {fruit}")
fruits = ["apple", "banana", "cherry"] fruits.each_with_index do |fruit, index| puts "#{index}: #{fruit}" end # Note: index comes SECOND in Ruby's block arguments fruits.each_with_index { |fruit, index| puts "#{index + 1}: #{fruit}" }
Ruby's .each_with_index passes the element first and index second — the opposite order from Python's enumerate() tuple unpacking. Ruby also has .each_with_object for accumulating results, and .map.with_index for indexed transformation.
zip()
names = ["Alice", "Bob", "Carol"] scores = [95, 87, 92] for name, score in zip(names, scores): print(f"{name}: {score}")
names = ["Alice", "Bob", "Carol"] scores = [95, 87, 92] names.zip(scores).each do |name, score| puts "#{name}: #{score}" end
Ruby's .zip is a method on the first array, taking the other arrays as arguments. It returns an array of arrays. The pattern a.zip(b).each { |x, y| ... } is equivalent to Python's for x, y in zip(a, b):. Ruby also has .each_with_object for building up results during iteration.
Comprehensions vs Enumerable
List comprehension vs map
numbers = [1, 2, 3, 4, 5] # List comprehension doubled = [n * 2 for n in numbers] print(doubled) # map() with lambda doubled2 = list(map(lambda n: n * 2, numbers)) print(doubled2)
numbers = [1, 2, 3, 4, 5] # map with a block (preferred) doubled = numbers.map { |number| number * 2 } puts doubled.inspect # Symbol#to_proc shorthand for named methods words = ["hello", "world"] puts words.map(&:upcase).inspect
Ruby uses .map where Python uses list comprehensions or map(). The block syntax { |x| x * 2 } is roughly equivalent to a lambda. Ruby's &:method_name shorthand (map(&:upcase)) converts a symbol to a proc — a convenient replacement for map(lambda s: s.upper()).
Filter comprehension vs select
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # List comprehension with condition evens = [n for n in numbers if n % 2 == 0] print(evens) # filter() with lambda odds = list(filter(lambda n: n % 2 != 0, numbers)) print(odds)
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] evens = numbers.select { |number| number.even? } puts evens.inspect odds = numbers.reject { |number| number.even? } puts odds.inspect
Ruby uses .select (keep elements where block returns true) and .reject (drop elements where block returns true). Python has both list comprehensions with conditions and filter(). Note Ruby's predicate methods: .even?, .odd?, .zero?, .nil?, .empty? — the ? suffix signals a boolean-returning method.
reduce / inject
from functools import reduce numbers = [1, 2, 3, 4, 5] total = reduce(lambda acc, n: acc + n, numbers, 0) print(total) # 15 product = reduce(lambda acc, n: acc * n, numbers) print(product) # 120
numbers = [1, 2, 3, 4, 5] total = numbers.reduce(0) { |accumulator, number| accumulator + number } puts total # 15 # Symbol shorthand product = numbers.reduce(:*) puts product # 120 # inject is an alias for reduce puts numbers.inject(:+) # 15
Ruby's .reduce (also aliased as .inject) corresponds to Python's functools.reduce. The symbol shorthand .reduce(:+) is equivalent to passing the + method as an operator — cleaner than Python's lambda acc, n: acc + n. An initial value is the first argument: .reduce(0, :+).
any() / all()
numbers = [2, 4, 6, 8, 10] print(all(n % 2 == 0 for n in numbers)) # True print(any(n > 9 for n in numbers)) # True print(any(n < 0 for n in numbers)) # False
numbers = [2, 4, 6, 8, 10] puts numbers.all?(&:even?) # true puts numbers.any? { |n| n > 9 } # true puts numbers.any? { |n| n < 0 } # false puts numbers.none? { |n| n < 0 } # true
Ruby's .all?, .any?, and .none? mirror Python's all(), any(), and not any(). Python uses generator expressions; Ruby uses blocks. The ? suffix signals a predicate method that returns a boolean.
Method chaining
numbers = range(1, 11) # Chaining: filter then transform then sum result = sum(n * n for n in numbers if n % 2 == 0) print(result) # 220 (4+16+36+64+100) # Or with explicit steps evens = [n for n in numbers if n % 2 == 0] squared = [n * n for n in evens] total = sum(squared) print(total)
result = (1..10) .select(&:even?) .map { |number| number ** 2 } .sum puts result # 220 # Short form puts (1..10).select(&:even?).map { |number| number**2 }.sum
Ruby's chaining style — calling methods sequentially — is central to idiomatic Ruby. Each Enumerable method returns an array (or enumerator), enabling further chaining. Python achieves similar results with nested comprehensions or explicit variables, but Ruby's method chain reads left-to-right like prose.
Functions
def and return values
def add(a, b): return a + b def greet(name): message = f"Hello, {name}!" return message print(add(3, 4)) print(greet("Alice"))
def add(first, second) first + second # last expression is the return value end def greet(name) message = "Hello, #{name}!" message # returned implicitly end puts add(3, 4) puts greet("Alice")
Ruby methods return the value of their last expression implicitly — no return keyword needed (though it is valid for early returns). This encourages writing methods as a sequence of transformations ending with the desired value. The return keyword is used when exiting early from a method.
Default parameters
def greet(name, greeting="Hello", punctuation="!"): print(f"{greeting}, {name}{punctuation}") greet("Alice") # Hello, Alice! greet("Bob", "Hi") # Hi, Bob! greet("Carol", punctuation=".") # Hello, Carol.
def greet(name, greeting: "Hello", punctuation: "!") puts "#{greeting}, #{name}#{punctuation}" end greet("Alice") # Hello, Alice! greet("Bob", greeting: "Hi") # Hi, Bob! greet("Carol", punctuation: ".") # Hello, Carol.
Ruby uses keyword arguments (with colon suffix) as the idiomatic way to provide optional parameters with defaults — similar to Python's keyword arguments. Ruby also supports positional defaults (def f(x, y=10)). Unlike Python, Ruby does not allow calling keyword-only arguments as positional ones.
*args and **kwargs
def variadic(*args, **kwargs): print(args) # tuple of positional args print(kwargs) # dict of keyword args variadic(1, 2, 3, name="Alice", city="NYC") # Spreading a list/dict into a call numbers = [1, 2, 3] options = {"sep": "-"} print(*numbers)
def variadic(*args, **kwargs) puts args.inspect # array of positional args puts kwargs.inspect # hash of keyword args end variadic(1, 2, 3, name: "Alice", city: "NYC") # Spreading an array into a call numbers = [1, 2, 3] puts numbers.join("-") # 1-2-3 # Splat in method call def add(first, second, third) = first + second + third puts add(*numbers)
Ruby's *args collects extra positional arguments as an array (Python collects them as a tuple). Ruby's **kwargs collects keyword arguments as a hash. The splat *array in a method call expands an array into positional arguments — the same as Python's *list spreading.
Lambdas and procs
# Lambda square = lambda x: x ** 2 print(square(5)) # 25 # Multi-line lambda via function def make_adder(n): return lambda x: x + n add5 = make_adder(5) print(add5(10)) # 15
# Lambda square = ->(x) { x ** 2 } puts square.call(5) # 25 puts square.(5) # alternative call syntax # Multi-line via proc (or lambda) def make_adder(number) ->(x) { x + number } end add5 = make_adder(5) puts add5.call(10) # 15
Ruby's lambda literal is ->(args) { body }, called with .call() or the shorthand .(). Ruby also has Proc.new { |x| ... } which behaves differently (no strict arity, different return semantics). The -> (stabby lambda) is preferred for lambda-style anonymous functions.
One-liner method syntax
# Python: no special one-liner syntax; use lambda for simple cases double = lambda x: x * 2 is_even = lambda n: n % 2 == 0 # For readable named methods, always use full def def square(x): return x * x
# Ruby 4.0: one-liner method syntax (no end needed) def double(x) = x * 2 def square(x) = x * x def greet(name) = "Hello, #{name}!" puts double(5) # 10 puts square(4) # 16 puts greet("Alice") # Hello, Alice!
Ruby 3.0 introduced a one-liner method definition using = instead of a body and end. This is equivalent to Python's use of lambda for simple one-expression functions, but produces a proper named method. Use it when the entire body is a single expression.
Classes & OOP
Class definition
class Dog: def __init__(self, name, breed): self.name = name self.breed = breed def speak(self): return f"{self.name} says Woof!" dog = Dog("Rex", "Labrador") print(dog.speak()) print(dog.name)
class Dog def initialize(name, breed) @name = name @breed = breed end def speak "#{@name} says Woof!" end end dog = Dog.new("Rex", "Labrador") puts dog.speak puts dog.instance_variable_get(:@name)
Key differences: (1) __init__ becomes initialize; (2) self.x = y (Python attribute assignment) becomes @x = y (Ruby instance variable); (3) methods omit the explicit self parameter; (4) call Dog.new(), not Dog(); (5) last expression is returned implicitly.
@property vs attr_accessor
class Circle: def __init__(self, radius): self._radius = radius @property def radius(self): return self._radius @radius.setter def radius(self, value): if value < 0: raise ValueError("Radius must be non-negative") self._radius = value @property def area(self): return 3.14159 * self._radius ** 2 c = Circle(5) print(c.radius) print(c.area)
class Circle attr_reader :area def initialize(radius) self.radius = radius # calls the setter end def radius = @radius def radius=(value) raise ArgumentError, "Radius must be non-negative" if value < 0 @radius = value @area = 3.14159 * @radius ** 2 end end circle = Circle.new(5) puts circle.radius puts circle.area
Ruby's attr_reader :name, attr_writer :name, and attr_accessor :name generate getter/setter methods automatically. For simple cases, use attr_accessor instead of Python's @property boilerplate. For validated setters, define a method named name=(value) — Ruby routes obj.name = value to that method.
Inheritance
class Animal: def __init__(self, name): self.name = name def speak(self): return f"{self.name} makes a sound" class Cat(Animal): def speak(self): return f"{self.name} says Meow!" def purr(self): return f"{self.name} purrs..." cat = Cat("Whiskers") print(cat.speak()) print(cat.purr()) print(isinstance(cat, Animal)) # True
class Animal def initialize(name) @name = name end def speak = "#{@name} makes a sound" end class Cat < Animal def speak = "#{@name} says Meow!" def purr = "#{@name} purrs..." end cat = Cat.new("Whiskers") puts cat.speak puts cat.purr puts cat.is_a?(Animal) # true
Ruby uses < for inheritance (class Cat < Animal), Python uses parentheses (class Cat(Animal)). Both support single inheritance only at the class level; mixins provide Ruby's equivalent of Python's multiple inheritance. Override methods by redefining them; call the parent with super.
Class and static methods
class Temperature: def __init__(self, celsius): self.celsius = celsius @classmethod def from_fahrenheit(cls, fahrenheit): return cls((fahrenheit - 32) * 5 / 9) @staticmethod def absolute_zero(): return -273.15 temp = Temperature.from_fahrenheit(98.6) print(f"{temp.celsius:.2f}") print(Temperature.absolute_zero())
class Temperature attr_reader :celsius def initialize(celsius) @celsius = celsius end def self.from_fahrenheit(fahrenheit) new((fahrenheit - 32) * 5.0 / 9) end def self.absolute_zero = -273.15 end temp = Temperature.from_fahrenheit(98.6) puts "%.2f" % temp.celsius puts Temperature.absolute_zero
Ruby has no @classmethod/@staticmethod distinction — both are def self.method_name. Inside a class method, new refers to the class itself (like Python's cls(...)). Class methods are called directly on the class: Temperature.from_fahrenheit(98.6).
__str__ / __repr__ vs to_s / inspect
class Point: def __init__(self, x, y): self.x = x self.y = y def __str__(self): return f"({self.x}, {self.y})" def __repr__(self): return f"Point({self.x!r}, {self.y!r})" point = Point(3, 4) print(str(point)) # (3, 4) print(repr(point)) # Point(3, 4)
class Point attr_reader :x, :y def initialize(x, y) @x = x @y = y end def to_s = "(#{@x}, #{@y})" def inspect = "Point(#{@x.inspect}, #{@y.inspect})" end point = Point.new(3, 4) puts point.to_s # (3, 4) puts point.inspect # Point(3, 4) p point # Point(3, 4) — p calls inspect
Ruby's to_s is Python's __str__, and inspect is Python's __repr__. puts obj calls to_s; p obj calls inspect. Overriding to_s also improves string interpolation output since #{obj} calls to_s.
Modules & Mixins
import vs require
import math import os from pathlib import Path from collections import defaultdict print(math.pi) print(math.sqrt(16))
require "set" require "json" # Standard library available without require in IRB/pry; # in scripts, require what you use puts Math::PI puts Math.sqrt(16) # require_relative for files relative to current file # require_relative "./my_module"
Ruby uses require "library_name" (string, no path extension). Python uses import module_name. Ruby's core classes (String, Array, Hash, Integer) need no require. The standard library needs require for modules like "set", "json", "date". Use require_relative for your own files.
Multiple inheritance vs include
class Flyable: def fly(self): return f"{self.__class__.__name__} is flying" class Swimmable: def swim(self): return f"{self.__class__.__name__} is swimming" class Duck(Flyable, Swimmable): pass duck = Duck() print(duck.fly()) print(duck.swim())
module Flyable def fly = "#{self.class.name} is flying" end module Swimmable def swim = "#{self.class.name} is swimming" end class Duck include Flyable include Swimmable end duck = Duck.new puts duck.fly puts duck.swim puts duck.is_a?(Flyable) # true
Python achieves code reuse through multiple inheritance; Ruby uses module and include (mixins). Ruby modules cannot be instantiated — they exist solely to be mixed into classes. include adds module methods as instance methods; extend adds them as class methods. A class can include any number of modules.
Comparable mixin
# Python: define __lt__, __le__, etc., or use @functools.total_ordering from functools import total_ordering @total_ordering class Temperature: def __init__(self, celsius): self.celsius = celsius def __eq__(self, other): return self.celsius == other.celsius def __lt__(self, other): return self.celsius < other.celsius temps = [Temperature(30), Temperature(10), Temperature(20)] print(sorted(temps, key=lambda t: t.celsius)[0].celsius)
class Temperature include Comparable attr_reader :celsius def initialize(celsius) @celsius = celsius end # Only need to define <=>; Comparable provides all other operators def <=>(other) celsius <=> other.celsius end end temps = [Temperature.new(30), Temperature.new(10), Temperature.new(20)] puts temps.min.celsius # 10 puts temps.sort.map(&:celsius).inspect # [10, 20, 30]
Ruby's Comparable mixin is included with include Comparable. Implement the spaceship operator <=> (returning -1, 0, or 1) and you get <, <=, >, >=, between?, and clamp for free. Python's @functools.total_ordering achieves the same but requires defining two methods.
Error Handling
try / except vs begin / rescue
try: result = int("not a number") print(result) except ValueError as error: print(f"Caught: {error}") except (TypeError, RuntimeError) as error: print(f"Other error: {error}")
begin result = Integer("not a number") puts result rescue ArgumentError => error puts "Caught: #{error}" rescue TypeError, RuntimeError => error puts "Other error: #{error}" end
Ruby's begin/rescue/end is equivalent to Python's try/except. The error variable is bound with => in Ruby (rescue Error => error) vs Python's as (except Error as error). Note Integer("x") raises ArgumentError, not ValueError — Ruby's exception hierarchy differs from Python's.
finally vs ensure
try: file = open("/tmp/pyrb-test-file.txt", "w") file.write("data") except IOError as error: print(f"IO error: {error}") else: print("Write succeeded") finally: print("Always runs")
begin result = "simulated write" puts "Write result: #{result}" rescue IOError => error puts "IO error: #{error}" else puts "Write succeeded" ensure puts "Always runs" end
Ruby's ensure is equivalent to Python's finally — always executes regardless of exceptions. Ruby's else in a begin/rescue block runs only when no exception was raised — same as Python's else in a try block. Python's context managers (with open()) have no direct equivalent in Ruby, though File.open(path) { |f| ... } closes automatically.
Custom exceptions
class InsufficientFundsError(Exception): def __init__(self, amount, balance): self.amount = amount self.balance = balance super().__init__(f"Cannot withdraw {amount}, balance is {balance}") try: raise InsufficientFundsError(100, 50) except InsufficientFundsError as error: print(error)
class InsufficientFundsError < StandardError attr_reader :amount, :balance def initialize(amount, balance) @amount = amount @balance = balance super("Cannot withdraw #{amount}, balance is #{balance}") end end begin raise InsufficientFundsError.new(100, 50) rescue InsufficientFundsError => error puts error end
Both languages define custom exceptions by inheriting from a base class. Python inherits from Exception; Ruby inherits from StandardError (or a more specific subclass). super(message) passes the message to the base class. Ruby's exception hierarchy: Exception > StandardError > RuntimeError — rescue StandardError to catch all typical program errors.
raise / re-raise
def validate_age(age): if not isinstance(age, int): raise TypeError(f"age must be int, got {type(age).__name__}") if age < 0: raise ValueError(f"age must be non-negative, got {age}") return age try: try: validate_age(-5) except ValueError as error: print(f"Caught: {error}") raise # re-raise — bare raise preserves original traceback except ValueError as error: print(f"Propagated: {error}")
def validate_age(age) raise TypeError, "age must be Integer, got #{age.class}" unless age.is_a?(Integer) raise ArgumentError, "age must be non-negative, got #{age}" if age < 0 age end begin begin validate_age(-5) rescue ArgumentError => error puts "Caught: #{error}" raise # re-raise — bare raise preserves original traceback end rescue ArgumentError => error puts "Propagated: #{error}" end
Ruby's raise ExceptionClass, "message" is equivalent to Python's raise ExceptionClass("message"). A bare raise re-raises the current exception in both languages. Ruby's conventional exceptions for type errors and value errors are TypeError and ArgumentError respectively (Python uses TypeError and ValueError).
Pattern Matching
match / case vs case / in
# Python 3.10+ structural pattern matching command = {"action": "move", "direction": "north", "steps": 3} match command: case {"action": "move", "direction": direction, "steps": steps}: print(f"Moving {direction} by {steps} steps") case {"action": "stop"}: print("Stopping") case _: print("Unknown command")
command = { action: "move", direction: "north", steps: 3 } case command in { action: "move", direction: String => direction, steps: Integer => steps } puts "Moving #{direction} by #{steps} steps" in { action: "stop" } puts "Stopping" else puts "Unknown command" end
Ruby's pattern matching uses case/in (as opposed to the simpler case/when). Both Ruby and Python support structural pattern matching on hashes/dicts. Ruby's String => direction binds the matched value to a variable with type checking — similar to Python's direction: str() guard (though syntax differs).
Sequence patterns
point = [3, 4] match point: case [0, 0]: print("Origin") case [x, 0]: print(f"On x-axis at {x}") case [0, y]: print(f"On y-axis at {y}") case [x, y]: print(f"Point at ({x}, {y})")
point = [3, 4] case point in [0, 0] puts "Origin" in [x, 0] puts "On x-axis at #{x}" in [0, y] puts "On y-axis at #{y}" in [x, y] puts "Point at (#{x}, #{y})" end
Array/sequence patterns match almost identically in both languages. Variables in the pattern capture matched elements — [x, y] destructures the array into x and y. Python calls these "sequence patterns"; Ruby calls them "array patterns". Both support wildcard (_) for ignored positions.
Guard conditions
numbers = [1, 2, 3, 4, 5] match numbers: case [first, *rest] if first > 0: print(f"Starts positive: {first}, rest={rest}") case [first, *rest]: print(f"Non-positive start: {first}") case []: print("Empty")
numbers = [1, 2, 3, 4, 5] case numbers in [first, *rest] if first > 0 puts "Starts positive: #{first}, rest=#{rest.inspect}" in [first, *rest] puts "Non-positive start: #{first}" in [] puts "Empty" end
Guard conditions use if condition after the pattern in both languages — the pattern must match AND the guard must be true. The splat *rest captures remaining elements in both Python and Ruby. Ruby's pattern matching was added in Ruby 2.7 (finalized in 3.0); Python's match was added in 3.10.
Deconstruct in custom classes
# Python: use __match_args__ for positional matching class Point: __match_args__ = ("x", "y") def __init__(self, x, y): self.x = x self.y = y point = Point(3, 4) match point: case Point(x=0, y=0): print("Origin") case Point(x=px, y=py): print(f"Point({px}, {py})")
class Point attr_reader :x, :y def initialize(x, y) @x = x @y = y end # deconstruct_keys enables hash-pattern matching def deconstruct_keys(keys) = { x: @x, y: @y } end point = Point.new(3, 4) case point in { x: 0, y: 0 } puts "Origin" in { x: px, y: py } puts "Point(#{px}, #{py})" end
Both languages allow custom classes to participate in structural pattern matching. Python uses __match_args__ for positional matching. Ruby uses deconstruct_keys(keys) (for hash patterns) and deconstruct (for array patterns). Implement whichever your class's natural structure maps to.