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.