Ruby.CodeCompared.To/Python

An interactive executable cheatsheet for Rubyists learning Python

Ruby 4.0 Python 3.13
Syntax Basics
Blocks / Indentation
def greet(name) "Hello, #{name}" end puts greet("Alice")
def greet(name): return f"Hello, {name}" print(greet("Alice"))
Python uses mandatory indentation (4 spaces by convention) instead of end. Whitespace is syntax.
Line Terminators
x = 1 y = 2; z = 3 # optional semicolons puts x, y, z
x = 1 y = 2; z = 3 # valid but frowned upon print(x, y, z)
Neither language requires semicolons. The Python style guide (PEP 8) discourages them. The Ruby community ignores them too.
Comments
# single line =begin multi-line block =end
# single line """ multi-line (docstring or triple-quoted string used as comment) """ print("comments work")
Python has no block-comment syntax; triple-quoted strings are the convention. =begin/=end is rarely used in Ruby.
Variable Assignment
x = 10 a, b, c = 1, 2, 3 # parallel first, *rest = [1,2,3,4] # splat puts "#{x} #{a} #{b} #{c} #{first} #{rest}"
x = 10 a, b, c = 1, 2, 3 # unpacking first, *rest = [1,2,3,4] # splat (same!) print(x, a, b, c, first, rest)
Splat/star unpacking is nearly identical. Python 3.0 added extended iterable unpacking in PEP 3132, which cited Ruby's splat assignment as one of its inspirations.
Constants
MAX = 100 MAX = 150 # Ruby warns but won't stop reassignment puts MAX
MAX = 100 # Python convention only β€” no enforcement print(MAX)
Neither language truly enforces constants. Ruby gives a warning on reassignment; Python silently allows it.
Truthiness
# Only nil and false are falsy zero = 0; puts(zero ? "truthy" : "falsy") # truthy blank = ""; puts(blank ? "truthy" : "falsy") # truthy empty = []; puts(empty ? "truthy" : "falsy") # truthy
# Many things are falsy in Python print("0: ", "truthy" if 0 else "falsy") # falsy! print('"": ', "truthy" if "" else "falsy") # falsy! print("[]: ", "truthy" if [] else "falsy") # falsy! print("1: ", "truthy" if 1 else "falsy") # truthy
Major gotcha. In Ruby, only nil and false are falsy. In Python, 0, "", [], {}, and None are all falsy.
Nil / None
x = nil puts x.nil? # => true puts x.is_a?(NilClass) # => true
x = None print(x is None) # True (use `is`, not ==) print(type(x) == type(None))
Always use is None / is not None in Python, not ==. Ruby's .nil? has no direct Python equivalent.
Logical Operators
x = true; y = false puts x && y # and (short-circuit) puts x || y # or puts !x # not # also: 'and', 'or', 'not' (lower precedence)
x = True; y = False print(x and y) # False print(x or y) # True print(not x) # False # No &&, ||, or ! symbols
Python uses plain English keywords only β€” no && / ||. Ruby has both, but and/or have very low precedence; prefer &&/|| in Ruby.
Ternary
x = 5 result = x > 0 ? "positive" : "negative" puts result
x = 5 result = "positive" if x > 0 else "negative" print(result)
Python's ternary reads like English. There is no ?: operator.
String Interpolation
name = "Alice" puts "Hello, #{name}!" puts "2 + 2 = #{2 + 2}"
name = "Alice" print(f"Hello, {name}!") print(f"2 + 2 = {2 + 2}")
Python f-strings (3.6+) require the f prefix. Both support arbitrary expressions inside {}.
Types & Data
Type Check
x = 42 puts x.is_a?(Integer) # => true puts x.class # => Integer puts x.kind_of?(Numeric) # => true
x = 42 print(isinstance(x, int)) # preferred print(type(x) == int) # exact, not subclass-aware print(isinstance(x, (int, float))) # multi-type check
Always prefer isinstance() in Python β€” it respects inheritance, just like Ruby's is_a?/kind_of?.
Integer
puts 1_000_000 # underscores ok puts 42.class # => Integer puts 10 / 3 # => 3 (integer division)
print(1_000_000) # underscores ok (3.6+) print(type(42)) # => int print(10 // 3) # => 3 (floor division) print(10 / 3) # => 3.333... (always float!)
Gotcha: / in Python 3 always returns a float. Use // for integer division.
Float
puts 3.14.round(2) puts 3.14.ceil puts 3.14.floor
import math print(round(3.14159, 2)) print(math.ceil(3.14)) print(math.floor(3.14))
Ruby's float methods are called on the object; Python uses standalone functions (or the math module).
Symbols
puts :name puts :name.to_s # => "name" puts "name".to_sym # => name
from enum import Enum # Use strings for dict keys; Enum for named constants class Direction(Enum): NORTH = "north" SOUTH = "south" print(Direction.NORTH) print(Direction.NORTH.value)
Python has no symbol type. Strings are interned for small values anyway. Use Enum for symbolic constants.
Boolean
puts true puts false puts true.class # => TrueClass
print(True) # capital T print(False) # capital F print(type(True)) # => bool
Capitalization! Python booleans are True and False. Writing true in Python raises a NameError.
Type Conversion
puts "42".to_i # => 42 puts 42.to_s # => "42" puts 42.to_f # => 42.0 puts "3.14".to_f # => 3.14
print(int("42")) # => 42 print(str(42)) # => "42" print(float(42)) # => 42.0 print(float("3.14")) # => 3.14
Python uses constructor-style functions; Ruby uses .to_* methods. Ruby's "abc".to_i returns 0; Python's int("abc") raises a ValueError.
Comparable / Ordering
class Box include Comparable attr_reader :volume def initialize(volume) = @volume = volume def <=>(other) = volume <=> other.volume end boxes = [Box.new(10), Box.new(3), Box.new(7)] puts boxes.sort.map(&:volume).inspect
from functools import total_ordering @total_ordering class Box: def __init__(self, volume): self.volume = volume def __eq__(self, other): return self.volume == other.volume def __lt__(self, other): return self.volume < other.volume boxes = [Box(10), Box(3), Box(7)] print([b.volume for b in sorted(boxes, key=lambda b: b.volume)])
Ruby's Comparable mixin only needs <=>. Python needs @total_ordering + two methods, or define all six comparison dunders.
Strings
Immutability
# Mutable by default greeting = "hello" greeting
# Always immutable β€” += creates a new object greeting = "hello" original_id = id(greeting) greeting += " world" print(greeting) print("New object?", id(greeting) != original_id)
Python strings are always immutable. All string operations return new objects. Ruby strings are mutable by default (use .freeze or the frozen string literal magic comment to match Python's behavior).
Multiline Strings
message = "line one line two" puts message message = <<~HEREDOC line one line two HEREDOC puts message
message = "line one line two" print(message) message = ( "line one " "line two" ) print(message)
Adjacent Python string literals auto-concatenate with no operator needed. Ruby's squiggly heredoc (<<~) strips leading whitespace.
Common Methods
text = "Hello, World" puts text.upcase puts text.downcase puts " hi ".strip puts text.include?("World") puts text.start_with?("Hello") puts text.gsub(/o/, "0") p text.split(", ") puts text.length
text = "Hello, World" print(text.upper()) print(text.lower()) print(" hi ".strip()) print("World" in text) # in-operator, not a method print(text.startswith("Hello")) import re; print(re.sub(r"o", "0", text)) print(text.split(", ")) print(len(text)) # len() is a standalone function
Python uses len() as a standalone function, not a method. The in operator tests membership. Regex requires import re.
Formatting
name = "Alice" puts "%.2f" % 3.14159 puts "Name: %s" % name puts format("%-10s|", name)
name = "Alice" print(f"{3.14159:.2f}") print(f"Name: {name}") print(f"{name:10}|") # right-aligned
f-strings are the modern Python approach. The %-style and .format() still work but f-strings are preferred in Python 3.6+.
Encoding
# Ruby 4.0: UTF-8 default, strings frozen by default text = "cafΓ©" puts text.encoding # => UTF-8 puts text.bytes.length # => 5 (bytes) puts text.length # => 4 (characters)
text = "cafΓ©" encoded = text.encode('utf-8') # str -> bytes print(type(encoded), encoded) decoded = encoded.decode('utf-8') # bytes -> str print(type(decoded), decoded)
Python 3 strictly separates str (unicode text) from bytes (raw bytes). Ruby strings can hold arbitrary bytes with an encoding tag attached.
Collections
List Basics
numbers = [1, 2, 3, 4, 5] numbers << 6 puts numbers.first puts numbers.last puts numbers.length
numbers = [1, 2, 3, 4, 5] numbers.append(6) print(numbers[0]) # first print(numbers[-1]) # last print(len(numbers))
Python's list is like Ruby's Array. Use append instead of <<. Negative indices count from the end.
Dictionary Basics
person = { name: "Alice", age: 30 } person[:city] = "NYC" puts person[:name] puts person.keys.inspect
person = {"name": "Alice", "age": 30} person["city"] = "NYC" print(person["name"]) print(list(person.keys()))
Python's dict is like Ruby's Hash. Keys are usually strings (not symbols). The dict.keys() method returns a view object.
Array / List
numbers = [1, 2, 3] numbers.push(4) # or numbers 4 puts numbers.first, numbers.last p numbers[1..2] # range slice p [[1,2],[3]].flatten p [1, nil, 3].compact # removes nils
numbers = [1, 2, 3] numbers.append(4) numbers.pop() print(numbers[0], numbers[-1]) print(numbers[1:4]) # stop is exclusive nested = [[1,2],[3,4]] flat = [x for sub in nested for x in sub] print(flat) print([x for x in numbers if x is not None])
Python lists use append, not push/<<. Slices use [start:stop] where stop is exclusive. There is no .compact β€” filter manually.
Hash / Dict
person = {name: "Alice", age: 30} puts person[:name] p person.keys p person.values merged = person.merge({role: "admin"}) p merged p person.select { |key, value| value.is_a?(String) }
person = {"name": "Alice", "age": 30} print(person["name"]) print(person.get("missing")) # => None (no exception) print(person.get("missing", "?")) # => "?" # no .dig β€” chain .get(): address = {"city": "Portland"} data = {"address": address} print(data.get("address", {}).get("city")) merged = {**person, "role": "admin"} # merge print(merged) print({k: v for k, v in person.items() if isinstance(v, str)})
Python dicts use string keys by default (no symbols). The .get() method is Ruby's .fetch(key, default). Dict comprehensions mirror Ruby's .select/.transform_values.
Set
require 'set' numbers_set = Set.new([1, 2, 3]) numbers_set.add(4) puts numbers_set.include?(3) s1 = Set.new([1,2,3]); s2 = Set.new([2,3,4]) p s1 & s2 # intersection p s1 | s2 # union p s1 - s2 # difference
numbers_set = {1, 2, 3} # set literal numbers_set.add(4) print(3 in numbers_set) s1 = {1, 2, 3}; s2 = {2, 3, 4} print(s1 & s2) # intersection print(s1 | s2) # union print(s1 - s2) # difference print(set()) # empty set β€” NOT {} (that's a dict!)
Python sets are a built-in type with literal syntax ({1,2,3}). Ruby requires require 'set'. An empty set in Python is set(), NOT {} β€” that is an empty dict.
Tuple
# No tuple type β€” arrays fill this role coordinates = [1, 2, 3] # mutable coordinates.freeze # immutable Point = Struct.new(:x, :y) # named tuple equiv puts Point.new(3, 7)
coordinates = (1, 2, 3) # immutable coordinates = 1, 2, 3 # parens optional from collections import namedtuple Point = namedtuple('Point', ['x', 'y']) point = Point(3, 7) print(point.x, point.y) print(point) # Point(x=3, y=7)
Python tuples are immutable, fixed-size sequences used for heterogeneous data. Ruby arrays serve both roles. namedtuple β‰ˆ Ruby's Struct.
Ranges
p (1..10).to_a # inclusive p (1...10).to_a # exclusive end puts ('a'..'z').include?('m') # endless range: (1..).each { ... }
print(list(range(1, 11))) # exclusive end (like ...) print(list(range(1, 11, 2))) # step print(10 in range(1, 11)) # no string ranges import itertools counter = itertools.count(1) # endless print(next(counter), next(counter), next(counter))
Python's range is always exclusive-end (like Ruby's ...). Python ranges don't support strings or characters. Both languages make ranges lazy.
Enumerable / Iteration
numbers = [1, 2, 3, 4, 5] puts numbers.map { |x| x * 2 }.inspect puts numbers.select { |x| x > 3 }.inspect puts numbers.reject { |x| x > 3 }.inspect puts numbers.reduce(0) { |acc, x| acc + x } puts numbers.sum puts numbers.flat_map { |x| [x, x * 2] }.inspect puts numbers.each_with_index.map { |x, i| [i, x] }.inspect puts numbers.any? { |x| x > 4 } puts numbers.all? { |x| x > 0 } puts numbers.tally.inspect
from functools import reduce from collections import Counter numbers = [1, 2, 3, 4, 5] print([x * 2 for x in numbers]) print([x for x in numbers if x > 3]) print([x for x in numbers if not x > 3]) print(reduce(lambda acc, x: acc + x, numbers, 0)) print(sum(numbers)) print([y for x in numbers for y in [x, x * 2]]) print(list(enumerate(numbers))) print(any(x > 4 for x in numbers)) print(all(x > 0 for x in numbers)) print(Counter(numbers).most_common())
Python favors list comprehensions and generator expressions over chained method calls. map/filter exist but comprehensions are idiomatic. There is no .tally β€” use Counter.
Counter / Tally
p ["a","b","a","c"].tally # => {"a"=>2, "b"=>1, "c"=>1}
from collections import Counter counts = Counter(["a","b","a","c"]) print(counts) # Counter({'a': 2, ...}) print(counts.most_common(2)) # top 2 counts2 = Counter(["a","b"]) print(counts + counts2) # arithmetic!
Counter is more powerful than .tally β€” it supports arithmetic between counters and the most_common(n) method.
Struct / Dataclass
Point = Struct.new(:x, :y) point = Point.new(3, 7) puts point puts point.x, point.y
from dataclasses import dataclass, field @dataclass class Point: x: int y: int label: str = "origin" # default value point = Point(3, 7) print(point) print(point.x, point.y)
@dataclass is the modern Python equivalent of Struct. It is fully mutable and supports defaults, __repr__, __eq__, and more out of the box.
Control Flow
if / elsif / else
x = 0 if x > 0 puts "positive" elsif x < 0 puts "negative" else puts "zero" end
x = 0 if x > 0: print("positive") elif x < 0: print("negative") else: print("zero")
elif not elsif β€” an easy muscle-memory bug when coming from Ruby.
unless
logged_in = false unless logged_in puts "Please log in" end # inline suffix form: puts "done" unless false
logged_in = False if not logged_in: # no 'unless' keyword print("Please log in") # no suffix/inline conditionals either
Python has no unless and no suffix conditionals. Rewrite as if not. Many Rubyists miss the inline return if form.
Case / Match
status = 404 case status when 200 then result = "Good" when 500 then result = "Bad" when 400..499 then result = "Client error" else result = "Unknown" end puts result
status = 404 match status: case 200: result = "Good" case 500: result = "Bad" case x if 400 <= x <= 499: result = "Client error" case _: result = "Unknown" print(result)
Python's match (3.10+) supports structural pattern matching β€” more powerful than Ruby's case/when. Use if/elif chains for pre-3.10 compatibility.
Loops
x = 0 while x < 10 x += 1 end puts x
x = 0 while x < 10: x += 1 print(x)
Python has no until, no loop do, and no 5.times. Python's for iterates over any iterable β€” it is more like Ruby's each.
break / next / continue
[1,2,3,4,5].each do |i| next if i == 2 # skip break if i == 4 # stop puts i end
for i in [1, 2, 3, 4, 5]: if i == 2: continue # next in Ruby if i == 4: break print(i)
Python uses continue (not next). There is no loop-return-value. Python also has for/else β€” the else block runs only if the loop was not broken.
Walrus / Safe Nav
text = "order #12345" if (match = text.match(/d+/)) puts "Found: #{match[0]}" end user = nil puts user&.name # safe navigation (prints nil)
import re text = "order #12345" if m := re.search(r"d+", text): # walrus := print(f"Found: {m.group(0)}") user = None name = user.name if user else None # no &. operator print(name)
Python's walrus operator (:=) assigns and tests in one expression β€” handy for regex matches and comprehensions. Python has no &. safe navigation operator.
Methods & Functions
Define a Method
def add(first_number, second_number) first_number + second_number end puts add(3, 4)
def add(first_number, second_number): return first_number + second_number print(add(3, 4))
Python requires an explicit return. The last evaluated expression is not automatically returned as in Ruby.
Definition
def add(a, b) a + b # implicit return end puts add(3, 4)
def add(a, b): return a + b # explicit return required print(add(3, 4))
Python has no implicit return. A function without a return statement returns None silently. Always write return.
Default Args
def greet(name, greeting: "Hello") "#{greeting}, #{name}" end puts greet("Alice") puts greet("Alice", greeting: "Hi")
def greet(name, greeting="Hello"): return f"{greeting}, {name}" print(greet("Alice")) print(greet("Alice", greeting="Hi"))
Similar syntax, but Python defaults are positional by default. Gotcha: mutable defaults (lists, dicts) are shared across all calls β€” use None as a sentinel instead.
Keyword Args
def connect(host:, port: 80) "#{host}:#{port}" end puts connect(host: "localhost", port: 3000)
def connect(*, host, port=80): # * forces keyword-only return f"{host}:{port}" print(connect(host="localhost", port=3000)) print(connect(host="example.com"))
Python uses a bare * to separate positional from keyword-only arguments. Ruby keyword arguments always end with a colon.
Splat / *args
def total(*nums) nums.sum end def log(msg, **opts) "#{msg} #{opts}" end puts total(1, 2, 3) puts log("hi", level: "info")
def total(*nums): return sum(nums) def log(msg, **opts): return f"{msg} {opts}" print(total(1, 2, 3)) print(log("hi", level="info"))
Nearly identical. *args collects positional arguments; **kwargs collects keyword arguments into a dict. Python conventionally names them args and kwargs.
Return Values
def bounds(numbers) [numbers.min, numbers.max] end lo, hi = bounds([3, 1, 4, 1, 5]) puts "#{lo}..#{hi}"
def bounds(numbers): return min(numbers), max(numbers) # returns a tuple lo, hi = bounds([3, 1, 4, 1, 5]) print(f"{lo}..{hi}")
Both languages support multiple return values via unpacking. Python's idiom returns a tuple; Ruby's returns an array.
Type Hints
# No built-in type hints; use Sorbet or RBS # sig { params(a: Integer, b: Integer).returns(Integer) } def add(a, b) = a + b puts add(3, 4) puts add("hello", " world") # also works β€” no enforcement
def add(a: int, b: int) -> int: return a + b # Hints are NOT enforced at runtime: print(add(3, 4)) print(add("hello", " world")) # still works!
Python type hints are built-in syntax but are not enforced at runtime. Use mypy or pyright for static checking. Ruby's type story (Sorbet/RBS) is opt-in.
First-class Functions
double = ->(x) { x * 2 } puts double.call(5) puts double.(5) # shorthand [1,2,3].each(&method(:puts))
double = lambda x: x * 2 print(double(5)) # assign a def to a variable: def triple(x): return x * 3 transform = triple print(transform(5)) print(list(map(double, [1, 2, 3])))
Python lambdas are single-expression only. For multi-line callables, assign a def to a variable. Ruby procs/lambdas are more capable.
Blocks, Procs & Closures
Block / yield
def with_logging puts "start" result = yield puts "done: #{result}" end with_logging { 42 }
def with_logging(callable): print("start") result = callable() print(f"done: {result}") with_logging(lambda: 42)
Python has no block/yield equivalent (yield means something different in Python β€” generators). Pass callables explicitly instead.
Proc vs Lambda
callback = Proc.new { |x| x * 2 } doubler = lambda { |x| x * 2 } doubler = ->(x) { x * 2 } puts doubler.call(5) # Lambda: strict arity, return is local # Proc: loose arity, return exits method
doubler = lambda x: x * 2 print(doubler(5)) # Python lambda β‰ˆ Ruby lambda (local return, strict arity) # No Proc equivalent β€” use a nested def for complex closures
Python's lambda always behaves like Ruby's lambda β€” local return, strict-ish arity. There is no Proc analog. Use a nested def for complex closures.
Context Manager (vs Ruby ensure)
require 'stringio' # Block form auto-closes the resource: StringIO.open("hello") do |stream| puts stream.read end # auto-closed here # Manual equivalent with ensure: begin stream = StringIO.new("world") puts stream.read ensure stream&.close end
from contextlib import contextmanager @contextmanager def timed_block(label): import time start = time.time() yield elapsed = time.time() - start print(f"{label}: {elapsed:.4f}s") with timed_block("demo"): total = sum(range(100_000)) print(f"sum = {total}")
Python's with statement equals Ruby's block-form resource management. @contextmanager is the cleanest way to build custom context managers, and is analogous to wrapping yield.
Closures & Scope
x = 10 counter = -> { x += 1; x } puts counter.call # => 11 puts counter.call # => 12 (closes over x)
def make_counter(start): count = start def counter(): nonlocal count # required to rebind outer variable count += 1 return count return counter my_counter = make_counter(10) print(my_counter()) # => 11 print(my_counter()) # => 12
Gotcha: Python requires nonlocal to reassign a closed-over variable. Without it, the assignment creates a new local. Ruby closes over variables naturally.
Generators
counter = Enumerator.new do |yielder| n = 0 loop { yielder << n; n += 1 } end puts counter.next # => 0 puts counter.next # => 1 puts counter.next # => 2
def counter(): n = 0 while True: yield n n += 1 gen = counter() print(next(gen)) # 0 print(next(gen)) # 1 print(next(gen)) # 2
Python's yield keyword creates a generator β€” a lazy sequence. Generators are far more common in Python than Ruby's Enumerator. Generator expressions work like lazy list comprehensions.
Object-Oriented Programming
Class Definition
class Animal attr_accessor :name def initialize(name) @name = name end def speak = "..." end animal = Animal.new("Rex") puts animal.name
class Animal: def __init__(self, name): self.name = name # no @, no attr_accessor def speak(self): return "..." animal = Animal("Rex") # no 'new' keyword print(animal.name)
Python's __init__ = Ruby's initialize. Python requires explicit self as the first parameter of every instance method. There is no new keyword.
Instance Variables
class Person @@species = "Homo sapiens" # class variable attr_accessor :name # getter + setter def initialize(name) = @name = name def label = "Person: #{name}" def self.species = @@species end person = Person.new("Alice") puts person.name puts person.label puts Person.species
class Person: species = "Homo sapiens" # class variable def __init__(self, name): self.name = name # public instance var @property def label(self): # computed getter return f"Person: {self.name}" @label.setter def label(self, value): self.name = value.replace("Person: ", "") person = Person("Alice") print(person.name) print(person.label) print(Person.species)
Python has no attr_accessor. Use @property for computed or controlled access. Plain self.x is fully public β€” there is no access modifier for simple attributes.
Access Control
class Vault def public_info = "public" protected def internal = "protected" private def secret = "private" end vault = Vault.new puts vault.public_info # vault.secret # => NoMethodError (private)
class Vault: def public_info(self): return "public" def _internal(self): # convention: protected return "protected" def __secret(self): # name-mangled: private-ish return "private" vault = Vault() print(vault.public_info()) print(vault._internal()) # works, but "please don't" # vault.__secret() # AttributeError print(vault._Vault__secret()) # name-mangling workaround
Python uses naming conventions, not keywords. A single underscore means "please don't use externally." Double underscore triggers name-mangling β€” it is not truly private. All methods are technically accessible.
Inheritance
class Animal attr_reader :name def initialize(name) = @name = name def speak = "..." end class Dog < Animal def speak = "Woof!" end puts Dog.new("Rex").speak
class Animal: def __init__(self, name): self.name = name def speak(self): return "..." class Dog(Animal): def speak(self): return "Woof!" print(Dog("Rex").speak())
Python uses (ParentClass) syntax; Ruby uses <. A bare super() in Python infers arguments. Ruby's bare super forwards all args; super() forwards none.
Multiple Inheritance
# No multiple inheritance β€” use mixins instead module Flyable def fly = "I can fly" end module Swimmable def swim = "I can swim" end class Duck include Flyable include Swimmable end donald = Duck.new puts donald.fly puts donald.swim p Duck.ancestors
class Flyable: def fly(self): return "I can fly" class Swimmable: def swim(self): return "I can swim" class Duck(Flyable, Swimmable): pass donald = Duck() print(donald.fly()) print(donald.swim()) print(Duck.__mro__) # Method Resolution Order
Python supports true multiple inheritance with C3 linearization (MRO). Ruby uses single inheritance plus mixins. Python's approach is more powerful but more complex.
Class Methods
class User def self.create(name) new(name) end def initialize(name) = @name = name def to_s = @name end puts User.create("Alice")
class User: def __init__(self, name): self.name = name @classmethod def create(cls, name): # cls = the class itself return cls(name) @staticmethod def validate_name(name): # no cls or self return len(name) > 0 def __repr__(self): return f"User({self.name!r})" print(User.create("Alice")) print(User.validate_name("Alice"))
@classmethod receives cls β€” like Ruby's self.method. @staticmethod receives nothing β€” like a namespaced plain function with no access to the class or instance.
Duck Typing
class Cat; def speak = "Meow"; end class Dog; def speak = "Woof"; end def make_noise(thing) thing.speak end puts make_noise(Cat.new) puts make_noise(Dog.new) puts Cat.new.respond_to?(:speak)
class Cat: def speak(self): return "Meow" class Dog: def speak(self): return "Woof" def make_noise(thing): return thing.speak() # works for any .speak print(make_noise(Cat())) print(make_noise(Dog())) print(hasattr(Cat(), 'speak'))
Both languages favor duck typing. Python adds abc.ABC and Protocol (3.8+) for optional structural typing without inheritance.
Magic Methods / Dunders
class Money attr_reader :amount def initialize(amount) = @amount = amount def to_s = "$#{amount}" def inspect = "Money(#{amount})" def +(other) = Money.new(amount + other.amount) def ==(other) = amount == other.amount end wallet = Money.new(5) + Money.new(3) puts wallet # to_s puts wallet.inspect # inspect
class Money: def __init__(self, amount): self.amount = amount def __repr__(self): # like inspect return f"Money({self.amount})" def __str__(self): # like to_s return f"${self.amount}" def __add__(self, other): # + operator return Money(self.amount + other.amount) def __eq__(self, other): return self.amount == other.amount wallet = Money(5) + Money(3) print(repr(wallet)) print(str(wallet))
Python uses dunder (double-underscore) methods for everything. __str__ = to_s; __repr__ = inspect. Many more: __len__, __iter__, __getitem__, __contains__, etc.
Modules, Mixins & Namespaces
Module / Import
require 'json' # require_relative './my_file' # loads from same dir module MyApp class Foo; end end puts MyApp::Foo.new.class puts JSON.generate({key: "value"})
import json import math from math import sqrt, pi from collections import namedtuple as NT print(json.dumps({"key": "value"})) print(sqrt(16)) print(pi)
Python's import system is file-based β€” each .py file is a module. The from x import y form is like Ruby's include-after-require pattern.
Mixin / include
module Greetable def greet "Hello, I'm #{name}" end end class Person include Greetable attr_reader :name def initialize(name) = @name = name end puts Person.new("Alice").greet
class Greetable: # mixin via inheritance def greet(self): return f"Hello, I'm {self.name}" class Person(Greetable): def __init__(self, name): self.name = name print(Person("Alice").greet())
Python achieves mixins via multiple inheritance. There is no include keyword β€” simply list the mixin class in the inheritance tuple.
Package Structure
β”œ lib/my_gem.rb β”œ lib/my_gem/version.rb β”œ Gemfile β”” my_gem.gemspec
β”œ mypackage/__init__.py β”œ mypackage/module.py β”” pyproject.toml
Python packages require an __init__.py (can be empty) to mark a directory as a package. pyproject.toml is the modern equivalent of a gemspec.
Error Handling
raise / rescue
begin Integer("abc") rescue ArgumentError => e puts "caught: #{e.message}" rescue StandardError => e retry # or raise ensure puts "always runs" end
try: int("abc") except ValueError as e: print(f"caught: {e}") except Exception as e: raise # re-raise else: print("success") # runs only if no exception finally: print("always runs")
Key mappings: rescue β†’ except, ensure β†’ finally, retry has no direct equivalent. Python adds an else clause that runs only when no exception was raised.
Custom Errors
class InsufficientFundsError < StandardError; end begin raise InsufficientFundsError, "need $20 more" rescue InsufficientFundsError => error puts error.message end
class InsufficientFundsError(Exception): pass try: raise InsufficientFundsError("need $20 more") except InsufficientFundsError as error: print(error)
Python's Exception = Ruby's StandardError. Never catch BaseException β€” it catches SystemExit and KeyboardInterrupt too.
Re-raise / Chaining
require 'json' begin JSON.parse("bad json") rescue => error puts "logged: #{error.message}" # raise # would re-raise end
import json class ParseError(Exception): pass try: try: json.loads("bad json") except json.JSONDecodeError as original: raise ParseError("Could not parse") from original except ParseError as e: print(e) print("Caused by:", e.__cause__)
A bare raise re-raises in both languages. Python's raise X from e chains exceptions, preserving the original cause in __cause__.
I/O & Files
Print / Output
puts "hello" # adds newline print "hello" # no newline p [1, 2, 3] # inspect output pp({a: 1, b: 2}) # pretty print
print("hello") # adds newline print("hello", end="") # no newline print() # blank line print(repr([1, 2, 3])) # like p import pprint; pprint.pprint({"a": 1, "b": 2})
print() in Python is a function and requires parentheses. p β‰ˆ print(repr(obj)). Python's print accepts sep= and end= keyword arguments.
JSON
require 'json' parsed = JSON.parse('{"name":"Alice"}') puts parsed["name"] puts JSON.generate({name: "Alice"})
import json parsed = json.loads('{"name":"Alice"}') # string -> dict print(parsed["name"]) print(json.dumps({"name": "Alice"}, indent=2))
json.loads/json.dumps work with strings; json.load/json.dump work with file objects. Note the s suffix β€” a common source of bugs.
Regex
"hello" =~ /e(l+)o/ puts $1 # => "ll" puts "hello".match(/e(l+)o/)[1] puts "hello".gsub(/l/, "r")
import re m = re.search(r"e(l+)o", "hello") print(m.group(1)) # => "ll" print(re.sub(r"l", "r", "hello")) # findall β€” no Ruby equivalent needed: print(re.findall(r"d+", "a1b22c333"))
Python requires import re. Always use raw strings (r"...") for patterns. There is no =~ operator or global $1 capture variables.
Concurrency & Async
Threads
results = [] mutex = Mutex.new workers = (1..5).map do |number| Thread.new do processed = number * 2 mutex.synchronize { results << processed } end end workers.each(&:join) puts results.sort.inspect
import threading results = [] lock = threading.Lock() def worker(number): processed = number * 2 with lock: results.append(processed) threads = [threading.Thread(target=worker, args=(i,)) for i in range(1, 6)] for thread in threads: thread.start() for thread in threads: thread.join() print(sorted(results))
Both MRI Ruby and CPython have a global lock (GVL/GIL). Threads are fine for I/O-bound work; use processes for CPU-bound parallelism.
Browser sandbox: threads run sequentially here. Output is correct; concurrent timing effects are not demonstrated.
Async / Await
# Ruby uses Fibers for cooperative concurrency. # The Async gem (by Samuel Williams) builds on Fibers. def make_task(label, step_count) Fiber.new do step_count.times do |step_index| puts "#{label}: step #{step_index + 1} of #{step_count}" Fiber.yield end "#{label} complete" end end task_a = make_task("task-A", 3) task_b = make_task("task-B", 2) # Simple round-robin cooperative scheduler active = [task_a, task_b] until active.empty? active.each do |fiber| result = fiber.resume puts " => #{result}" unless fiber.alive? end active.reject! { |fiber| !fiber.alive? } end
import asyncio async def fetch_data(label, delay): await asyncio.sleep(delay) return f"{label} done after {delay}s" async def main(): results = await asyncio.gather( fetch_data("task-A", 0.02), fetch_data("task-B", 0.01), ) for result in results: print(result) asyncio.run(main())
Python's asyncio is built-in and widely adopted. Ruby's Async gem builds on Fibers for cooperative concurrency. Python's async/await syntax is more mainstream; Ruby's approach is more explicit.
Ruby's Async gem is not bundled here β€” the Fiber example shows the same cooperative model it builds on. Python asyncio runs in Pyodide's event loop; await yields but tasks do not run in parallel.
Standard Library & Packages
Package Manager
gem install rails bundle install # from Gemfile bundle exec rails s
pip install flask pip install -r requirements.txt # Modern approach: use uv or poetry
Bundler is analogous to pip + virtualenv combined. The Python best practice is to always use a virtual environment (python -m venv .venv or uv).
Virtual Envs & Version Management
rbenv install 4.0.0 rbenv shell 4.0.0
pyenv install 3.12.0 pyenv shell 3.12.0
rbenv β‰ˆ pyenv (switch between language versions). Bundler β‰ˆ pip + venv combined (dependency isolation is automatic in Ruby via Bundler). The uv tool (Rust-based) is a fast modern alternative to pip + venv.
Date / Time
require 'date' puts Time.now puts Date.today puts Time.now.strftime("%Y-%m-%d")
from datetime import datetime, date, timedelta print(datetime.now()) print(date.today()) print(datetime.now().strftime("%Y-%m-%d")) # Arithmetic: print(date.today() + timedelta(days=7))
Python's datetime is stdlib but somewhat clunky. The arrow or pendulum packages are popular alternatives, like Ruby's ActiveSupport time helpers.
Web Frameworks
Rails β€” full-stack, convention-over-configuration Sinatra β€” micro framework Grape β€” REST API framework (standalone or mountable)
Django β€” full-stack, convention-over-configuration Flask β€” micro framework FastAPI β€” async REST APIs, automatic OpenAPI docs
Metaprogramming
Dynamic Methods
class Greeter define_method(:hello) { |name| "Hi #{name}" } end obj = Greeter.new puts obj.send(:hello, "Alice") # method_missing for catch-all
class Greeter: def __getattr__(self, name): # like method_missing if name.startswith("say_"): word = name[4:] return lambda: print(word) raise AttributeError(name) greeter = Greeter() greeter.say_hello() greeter.say_goodbye()
Python's __getattr__ β‰ˆ Ruby's method_missing. Python's metaprogramming is less idiomatic than Ruby's but fully capable.
Decorators
module Logging def log_calls(method_name) original = instance_method(method_name) define_method(method_name) do |*args| puts "calling #{method_name}" result = original.bind(self).call(*args) puts "returned #{result}" result end end end class Calculator extend Logging def add(first, second) first + second end log_calls :add end Calculator.new.add(3, 4)
from functools import wraps def log_calls(func): @wraps(func) def wrapper(*args, **kwargs): print(f"calling {func.__name__}") result = func(*args, **kwargs) print(f"returned {result}") return result return wrapper @log_calls def add(first, second): return first + second add(3, 4)
Python's @log_calls is syntactic sugar for add = log_calls(add). Ruby has no decorator syntax, but the same pattern works: define the method, then call log_calls :add to wrap it. Both achieve the same result β€” transparently modifying a method's behavior after definition.
Open Classes
class String def palindrome? self == self.reverse end end puts "racecar".palindrome?
# Python discourages monkey-patching built-in types. # Preferred: use a subclass or a standalone function. def is_palindrome(text): return text == text[::-1] print(is_palindrome("racecar")) print(is_palindrome("hello"))
Ruby's open classes are a first-class feature. Python technically allows monkey-patching but the community strongly discourages it. Use subclasses or standalone functions instead.
Introspection
class Animal def speak = "..." end dog = Animal.new p dog.methods.grep(/speak/) puts dog.respond_to?(:speak) p Animal.ancestors p Animal.instance_methods(false)
class Animal: def speak(self): return "..." dog = Animal() print([m for m in dir(dog) if not m.startswith('_')]) print(hasattr(dog, 'speak')) print(Animal.__mro__) import inspect print([n for n, _ in inspect.getmembers(dog, predicate=callable) if not n.startswith('_')])
Both languages have rich introspection. Python's dir() is like .methods. vars() β‰ˆ instance_variables. The inspect module provides deeper reflection.
⚠ Gotchas for Rubyists
Falsy values
zero = 0; puts(zero ? "truthy" : "falsy") # truthy! blank = ""; puts(blank ? "truthy" : "falsy") # truthy! empty = []; puts(empty ? "truthy" : "falsy") # truthy!
for val in [0, "", [], {}, set(), None, False]: print(f"{repr(val):10} => {'truthy' if val else 'falsy'}")
High impact. The most common gotcha. In Python, 0, "", [], {}, set(), and None are all falsy.
Division
puts 10 / 3 # => 3 puts 10 / 3.0 # => 3.333...
print(10 / 3) # => 3.3333 (always float!) print(10 // 3) # => 3 (floor division) print(10 % 3) # => 1 (modulo)
High impact. Python 3's / always returns a float. Use // for integer division.
Mutable default args
def add_item(item, accumulator = []) accumulator << item accumulator end # Ruby has the SAME gotcha! p add_item(1) # => [1] p add_item(2) # => [1, 2] β€” same array reused!
def add_item(item, accumulator=[]): accumulator.append(item) return accumulator print(add_item(1)) # [1] print(add_item(2)) # [1, 2] β€” same list reused!
High impact. Mutable default arguments are evaluated once at definition time in Python, not per-call. Always use None as a sentinel for mutable defaults.
True/False casing
puts true # lowercase puts false puts nil.inspect
print(True) # capital T print(False) # capital F print(None) # 'true' and 'false' are NameErrors in Python
Capitalization matters. Writing true in Python raises a NameError. Muscle memory will betray you frequently at first.
elsif vs elif
x = -3 if x > 0 puts "positive" elsif x < 0 puts "negative" else puts "zero" end
x = -3 if x > 0: print("positive") elif x < 0: print("negative") else: print("zero")
You are guaranteed to write elsif in Python at least 50 times before the new spelling sticks.
Return values
def double(x) x * 2 # implicitly returned end puts double(5)
def double_wrong(x): x * 2 # computes and discards! returns None def double_right(x): return x * 2 # explicit return required print(double_wrong(5)) # None print(double_right(5)) # 10
Silent bug. Forgetting return returns None with no error or warning. Type hints plus mypy or pyright will catch this.
Hash / dictionary keys
config = {name: "Alice"} puts config[:name] # => "Alice"
config = {"name": "Alice"} # string keys print(config["name"]) # config[:name] would raise TypeError β€” no symbols! # For dot-access, use types.SimpleNamespace: from types import SimpleNamespace conf = SimpleNamespace(name="Alice", port=3000) print(conf.name, conf.port)
Python dict keys are almost always strings. The config[:name] syntax raises a TypeError. Use SimpleNamespace or attrs for dot-access on data objects.
Loop variable scope
[1, 2, 3].each { |number| puts number * 10 } # number is NOT available here: puts defined?(number) # => nil # Block variables stay scoped to the block
for number in [1, 2, 3]: print(number * 10) # number is STILL available here: print(number) # => 3 β€” the loop variable leaks!
Python's for loop variable leaks into the enclosing scope. This is by design, not a bug, but it surprises Rubyists who expect block-scoped variables.
Outer variable reassignment
def demo x = 10 increment = -> { x = 20 } increment.call puts x # => 20 β€” lambda changed outer x end demo
def demo(): x = 10 def increment_wrong(): x = 20 # creates a new LOCAL x β€” outer x unchanged! def increment_right(): nonlocal x x = 20 increment_wrong() print(x) # => 10 (outer x untouched!) increment_right() print(x) # => 20 demo()
Sneaky bug. Assignment inside a nested function creates a new local variable in Python by default. You must declare nonlocal x explicitly to reassign the outer variable.
String mutability
# Ruby 4.0: string literals are frozen by default greeting = "hello" puts greeting.frozen? # => true # Use unary + or dup to get a mutable copy mutable = +"hello" alias_ref = mutable mutable << " world" puts mutable.equal?(alias_ref) # => true (same object mutated) puts mutable # => hello world
# Python strings are always immutable greeting = "hello" try: greeting[0] = "H" except TypeError as error: print(error) alias_ref = greeting greeting = greeting + " world" print(greeting is alias_ref) # False β€” new object print(greeting) # hello world
Ruby strings are mutable β€” << modifies the original object in place. Python strings are immutable β€” += creates a new object and rebinds the variable. The alias_ref test proves whether the original object changed.
Indentation
# Indentation is purely cosmetic in Ruby: def greet(name) "Hello, #{name}" # works fine! end puts greet("Alice")
def well_indented(): x = 1 return x print(well_indented()) # Mixing tabs and spaces causes a TabError. # Always use 4 spaces. Never tabs. # Change indentation to cause an IndentationError
Mixed tabs and spaces cause a TabError. Configure your editor to always insert 4 spaces and never tabs for Python files.