The crosswalk
Level: reference · for anyone who knows one of the two languages
One line: One idea per row — what Ruby calls it, what Python calls it, and the page here that runs both — so a concept you already understand in one language can be looked up rather than relearned.
Every row names an idea once and then gives its two spellings. The last column is this library's lesson on it, where a Ruby program and its Python twin print the answer side by side; where the Python library ↗ has its own page on the idea, that page is linked too, marked ↗. A dash means the language has no counterpart, which is itself the answer. The rows are grouped the way the chapters are; Topics A–Z has the same pages under every name you might search for.
Values and objects
| The idea |
Ruby |
Python |
The page here |
| Nothing |
nil — an object of NilClass; only nil and false are falsy |
None — and 0, "", [], {} are falsy too |
nil, false and truthiness |
| Everything is an object |
1.class, nil.class, Class.class, 1.+(2) |
type(1), type(None), type(int); operators are dunder methods |
Everything is an object |
| A name as a value |
:symbol — one object per name |
an interned str, or enum.Enum |
Symbols |
| Identity and value |
equal? · == · eql? · === |
is · == · __hash__ · nothing like === |
Four kinds of equality |
| Assignment |
shares the object; dup copies |
shares the object; copy.copy copies |
Variables are references |
| Immutability |
freeze, frozen?, FrozenError; literals are chilled |
by type: tuple, frozenset, dataclass(frozen=True) |
freeze |
| Strings |
mutable — << changes the object |
immutable — += makes a new one; str is not bytes ↗ |
Strings are mutable |
| Duck typing |
respond_to?, to_str/to_ary/to_proc |
hasattr, __iter__/__index__, Protocol |
Duck typing |
Methods and arguments
| The idea |
Ruby |
Python |
The page here |
| Calling |
parentheses optional; a bare name may be a call |
parentheses mandatory; a bare name is a reference |
Parentheses are optional |
| What a method returns |
its last expression |
None unless return says otherwise |
The last expression is the value |
| Parameters |
a, b = 1, *rest, key:, **opts, &blk |
a, b=1, *args, key, **kwargs, with / and * markers |
Arguments |
| Default arguments |
evaluated on every call |
evaluated once, at def — the mutable-default trap |
Defaults are evaluated each call |
| Naming |
sort! and empty? |
sort() returns None; is_ prefixes; no ? |
! and ? |
| Operators |
methods: def +(other), def <=>(other) |
dunders: __add__, __lt__, __radd__ |
Operators are methods |
| The receiver |
implicit self |
explicit self parameter |
self is implicit |
| A call on nothing |
x&.length |
x.length if x else None, getattr(x, "y", None) |
Safe navigation |
| Setters |
def x=(v); the assignment's value is the argument |
@property + .setter; assignment is a statement |
Setters return the argument |
| A method as a value |
method(:f), UnboundMethod#bind |
obj.f (bound), Cls.f (a plain function) |
Method objects |
Blocks, procs and lambdas
| The idea |
Ruby |
Python |
The page here |
| Passing code to a method |
a block, run with yield |
a callable argument |
Blocks are not objects |
| Anonymous functions |
lambda / -> (strict) and proc (lenient) |
lambda — one expression — or a nested def |
Procs and lambdas differ |
| Closures |
share outer variables; a block parameter is fresh per call |
nonlocal; loop variables bind late |
Closures capture variables |
| A method by name |
&:upcase |
str.upper, operator.methodcaller |
&:symbol |
| The implicit parameter |
it, _1 |
— |
it and numbered parameters |
| Leaving a block |
next, break, return |
continue, break, return |
next, break and return |
| Writing an iterator |
each + Enumerable, Enumerator.new { \|y\| … } |
a generator function with yield |
Writing an iterator |
| Partial application |
curry, >>, << |
functools.partial; no composition operator |
Currying and composition |
Collections
| The idea |
Ruby |
Python |
The page here |
| Arrays |
a[10] is nil; a[1, 2] is start and length |
a[10] raises; a[1:3] is start and stop — Slicing is not indexing ↗ |
Arrays and negative indexes |
| Hashes |
ordered; h[:x] is nil; Hash.new(0); fetch |
ordered; d["x"] raises; defaultdict; .get |
Hashes and default values |
| Ranges |
1..5 inclusive, 1...5 exclusive, endless 1.. |
range(1, 6), half-open only |
Ranges |
| Sets |
Set — core in 4.0, keeps insertion order |
{1, 2}, frozenset — unordered |
Set is a core class |
| Unpacking |
a, *b = arr; block parameters auto-splat |
a, *b = seq; for k, v in … |
Destructuring |
| Records |
Struct (mutable), Data (immutable) |
namedtuple, dataclass, dataclass(frozen=True) |
Struct and Data |
| Ordering |
<=> and Comparable |
__lt__ and functools.total_ordering |
Comparable and <=> |
| The iteration protocol |
each + the Enumerable mixin |
__iter__ + builtins; collections.abc.Sequence as a mixin |
Enumerable is a mixin |
| The toolbox |
map, select, group_by, tally, each_slice, zip |
comprehensions, itertools.groupby, Counter, batched, zip |
The Enumerable toolbox |
| Folding |
inject, sum (compensated), each_with_object |
functools.reduce, sum, math.fsum |
inject and each_with_object |
| Laziness |
opt in with lazy |
generators are lazy by default |
Lazy enumerators |
| External iteration |
Enumerator#next, StopIteration |
next(it), StopIteration |
External enumerators |
| Sorting |
sort_by; stability not guaranteed |
sorted(key=); stable — Sorting is not comparing ↗ |
Sort stability and sort_by |
Classes and the object model
| The idea |
Ruby |
Python |
The page here |
| Attributes |
@x is private; attr_accessor opens it |
attributes are public; @property computes one |
Instance variables are private |
| Visibility |
private means no explicit receiver; protected |
_x by convention; __x is mangled |
private means no receiver |
| Open classes |
reopen String and add a method |
builtins are closed; user classes are open |
Classes are open |
| Mixins |
include, extend, prepend |
multiple inheritance and the MRO |
Mixins |
| Lookup |
ancestors, super |
__mro__, super() |
Method lookup and super |
| Constants |
lexical scope, then ancestors |
UPPER_CASE by convention; Final has no runtime |
Constants and lexical scope |
| Class-level state |
@@x is shared with subclasses; class-level @x is not |
class attributes, shadowed by instance assignment |
Class variables are shared |
| Namespaces |
module Shop; class Cart |
packages and modules |
Modules as namespaces |
| Per-object methods |
the singleton class, class << self |
types.MethodType, classmethod, the metaclass |
Singleton classes |
| Construction |
new = allocate + initialize |
__new__ + __init__ |
new, allocate and initialize |
| Display |
to_s / inspect; puts / p |
__str__ / __repr__; print / repr — repr is not str ↗ |
to_s, inspect and p |
| Being a hash key |
eql? + hash |
__eq__ + __hash__ |
eql? and hash |
| Scoped patches |
refinements |
— (unittest.mock.patch is the nearest) |
Refinements |
| The root of everything |
BasicObject; Kernel |
object; the builtins module |
BasicObject and Kernel |
| Copies |
dup, clone, Marshal round-trip |
copy.copy, copy.deepcopy, pickle |
dup, clone and frozen state |
Errors and exceptions
| The idea |
Ruby |
Python |
The page here |
| Handling |
begin/rescue/else/ensure; retry |
try/except/else/finally; no retry |
rescue, ensure, else and retry |
| The default catch |
StandardError |
Exception; a bare except: is BaseException — Ctrl-C is a signal ↗ |
StandardError is the default |
| Raising |
raise "msg", raise Klass, "msg" |
raise Klass("msg") |
raise has four forms |
| Custom classes |
class AppError < StandardError |
class AppError(Exception); exception groups |
Custom exception classes |
| Chaining |
cause, set automatically |
__context__ automatically; raise … from |
Exceptions have a cause |
| Expression-level handling |
expr rescue default |
— (contextlib.suppress) |
rescue as a modifier |
| Non-local exit |
throw / catch |
an exception, or for … else |
throw and catch |
| Exiting |
exit, at_exit, SystemExit |
sys.exit, atexit, SystemExit |
exit, at_exit and SystemExit |
| Warnings |
warn, -w, Warning[:deprecated] |
warnings.warn, -W, DeprecationWarning |
Warnings and -w |
| The messages you will meet |
NoMethodError for nil, ArgumentError … |
AttributeError for None, TypeError … |
Common error messages |
Control flow and pattern matching
| The idea |
Ruby |
Python |
The page here |
| Calling by name |
send, public_send |
getattr(obj, name)(…) |
send and public_send |
| Defining methods at runtime |
define_method |
setattr(Cls, name, func) |
define_method |
| Ghost methods |
method_missing, respond_to_missing? |
__getattr__ |
method_missing |
| Evaluating in a context |
instance_eval, class_eval |
exec with explicit namespaces |
instance_eval and class_eval |
| Hooks |
inherited, included, method_added |
__init_subclass__, __set_name__, metaclasses |
Hooks |
| A small language |
instance_eval and blocks |
class bodies, decorators, with |
Building a DSL |
| Classes at runtime |
Class.new(Base) { … } |
type(name, bases, namespace) |
Classes at runtime |
| Looking inside |
methods, instance_variables, defined? |
dir, vars, inspect |
Introspection |
| A scope as an object |
Binding, eval |
locals(), eval(expr, globals, locals) |
Binding and eval |
| Tracing |
TracePoint |
sys.settrace, sys.monitoring |
TracePoint |
| Memory |
ObjectSpace, GC, WeakRef |
gc, weakref |
ObjectSpace and GC |
| Bytecode |
RubyVM::InstructionSequence — YARV |
dis, code objects |
The bytecode you can see |
| JITs |
YJIT, ZJIT |
the 3.13 JIT; free-threading |
JITs and the interpreter |
Concurrency and parallelism
| The idea |
Ruby |
Python |
The page here |
| Threads |
Thread, the GVL |
threading, the GIL |
Threads and the GVL |
| Locks and queues |
Mutex, Queue, ConditionVariable |
Lock, queue.Queue, Condition |
Mutex, Queue and ConditionVariable |
| Coroutines |
Fiber |
generators, async def |
Fibers are coroutines |
| Where an enumerator runs |
on a Fiber |
as a suspended frame |
Enumerators run on fibers |
| Per-thread state |
Thread#[] (fiber-local!), thread_variable_get |
threading.local, contextvars |
Thread locals are fiber locals |
| Isolated parallelism |
Ractor |
multiprocessing; subinterpreters |
Ractors share nothing |
| Processes |
fork, spawn, Process.wait |
os.fork, subprocess, os.waitpid |
Processes |
| Timeouts |
Timeout.timeout, Thread#kill |
join(timeout), futures with timeout= |
Timeouts and killing threads |
| A queue that refuses when full |
SizedQueue#push(job, true) raises ThreadError |
queue.Queue(maxsize=n).put_nowait raises queue.Full |
A SizedQueue that refuses |
IO, files and the system
| The idea |
Ruby |
Python |
The page here |
| Files |
File.open(p) { \|f\| … } |
with open(p) as f: — Opening a file ↗ |
File.open with a block |
| Printing |
puts, print, p, pp |
print, repr, pprint |
puts, print, p and pp |
| Arguments and environment |
ARGV, ENV, $0 |
sys.argv, os.environ, __name__ |
ARGV, ENV and the exit status |
| Other programs |
system, backticks, Open3 |
subprocess.run |
Running other programs |
| Standard input |
gets, $stdin, ARGF |
input(), sys.stdin, fileinput — Standard in, standard out, and pipes ↗ |
gets and ARGF |
| Paths |
Pathname, Dir.glob |
pathlib.Path — Filenames are not text ↗ |
Pathname, Dir and glob |
| Serialization |
JSON, YAML, Marshal |
json, no YAML, pickle, tomllib |
JSON, YAML and Marshal |
| Time |
Time, Date |
datetime, date, timedelta |
Time and date |
| Formatting |
"#{x}", format, % |
f-strings, format, % — The format mini-language ↗ |
String formatting |
| Text and encodings |
one String type carrying an encoding label — the Ruby text library ↗ |
str and bytes — str is not bytes ↗ |
Text lives in the text library |
| The idea |
Ruby |
Python |
The page here |
| Loading code |
require, require_relative, load |
import, importlib.reload |
require, require_relative and load |
| Packages |
gems, Bundler, Gemfile |
pip, pyproject.toml, venv — pyproject.toml ↗ |
Gems, Bundler and the Gemfile |
| Tests |
Minitest |
unittest |
Minitest and unittest |
| The command line |
ruby -e -c -w -n -p |
python3 -c -m -I -W — -c is not the prompt ↗ |
The command-line flags |
| The REPL |
irb |
python3 -i, >>> |
irb and the REPL |
| Versions |
1.9 → 4.0 |
3.8 → 3.14 |
Ruby versions — what changed |
| Backtraces |
caller, backtrace |
traceback, inspect.stack |
Backtraces and caller |
| Documentation |
comments, YARD, ri — no docstrings |
__doc__, help() |
No docstrings |
Numbers
Idioms and gotchas
| The idea |
Ruby |
Python |
The page here |
| The philosophy |
more than one way to do it |
one obvious way |
The Ruby way and the Pythonic way |
| Defaults |
x \|\|= value |
x = x or value, :=, setdefault |
\|\|= and nil guards |
| Pipelines |
tap, then |
— |
tap, then and chaining |
| Several results |
an Array |
a tuple |
Multiple return values |
| Keys |
"a" and :a are different keys |
one string type |
String and symbol keys |
| Style |
two spaces, snake_case, ?/! |
PEP 8, four spaces |
Style and naming |
| Comments |
#, =begin, __END__ |
#, docstrings |
Comments and documentation |
| The traps |
for a Python programmer |
for a Ruby programmer |
both pages, one measured line per trap |
The pattern across the whole table: where Python has one spelling, Ruby often has two — lambda and proc, && and and, equal?/==/eql?/===, dup and clone, puts and p — and the second spelling is where a Python programmer's model breaks. Where Ruby has one type, Python often has two — str/bytes, list/tuple, set/frozenset — and the second type is where a Ruby programmer's model breaks. Each of those pairs has a row above and a page behind it.