Skip to content

Class.new makes a class; a constant names it

Level: 301 · for a reader who knows class Foo; end and wants to build classes from data, in a loop or inside a method

One line: Class.new(Base) { ... } returns a class object with no name, Foo = klass gives it the name of the first constant it is assigned to, and const_set does the same under a module — while Python's type("Foo", (Base,), {...}) names the class at birth, so there is no anonymous phase and no first-name-sticks rule.

class Dog < Animal; ...; end does three things: it creates a Class object, runs the body with that class as self, and assigns the object to the constant Dog. Class.new(Animal) { ... } does the first two — the block is run with class_eval, so def inside it defines instance methods and the block is a closure over the surrounding locals — and leaves the third to you. Until a constant holds it, the class's name is nil; the first constant assignment sets the name permanently, and later aliases do not change it. Module.new makes a module the same way, Struct.new and Data.define are class factories built on the same primitive, and Object.const_set(:Name, klass) is the programmatic form of Name = klass, including under a module (Zoo.const_set(:Cat, ...) gives "Zoo::Cat"). What you cannot do: subclass Class itself, or use a module as a superclass, or name a class with anything but a constant. The program measures each of these and the two facts that follow — inherited fires before the name exists, and a Class.new inside a method makes a fresh class on every call.

Verified output of classes_at_runtime_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 1. k = Class.new(Animal) { def speak = "woof" }   k.new.speak = "woof"
 2. k.name -- anonymous so far                     nil
 3. Dog = k; k.name                                "Dog"
 4. Hound = Dog; Hound.name -- the first name sticks "Dog"
 5. Zoo.const_set(:Cat, cat); cat.name             "Zoo::Cat"
    Object.const_get("Zoo::Cat").equal?(cat)       true
 6. Dog.include(Module.new { def hi }); Dog.new.hi "hi", module name = nil
    Greeting = greeting; Dog.ancestors.first(2)    [Dog, Greeting]
 7. Point = Struct.new(:x, :y); Point.new(1, 2)    #<struct Point x=1, y=2>
    Coord = Data.define(:lat, :lng); Coord.new(1, 2) #<data Coord lat=1, lng=2>
 8. k.class / k.superclass / Class.new.superclass  Class / Animal / Object
    k.is_a?(Module) / k.instance_of?(Class)        true / true
 9. Animal.inherited saw, in order (k, then Kitten) [nil, "Kitten"]
10. Class.new { define_method(:triple) { |x| x * factor } } new.triple(3) = 9
11. Class.new(Class)                               TypeError: can't make subclass of Class
    Class.new(Comparable) -- a Module              TypeError: superclass must be an instance of Class (given an instance of Module)
12. Object.const_set(:lower, Class.new)            NameError: wrong constant name lower
13. Object.const_defined?(:Hound)                  true
    Object.send(:remove_const, :Hound) -> then defined? "Dog" / false
14. make_class.equal?(make_class) / make_class.name false / nil
15. Class.new(Animal) takes no keyword options     configure with a class method afterwards

Reading the output

Rows 1–4 are the naming rule: the class works before it has a name (row 1), name is nil (row 2), Dog = k names it (row 3), and Hound = Dog is just a second constant for the same object, whose name stays Dog (row 4). Row 5 is the same rule through const_set under a module, and const_get with a "Zoo::Cat" string walks the path back. Row 6: modules are made and named the same way, and an anonymous module can be included before it has a name. Row 7: Struct.new and Data.define are Class.new with a body written for you. Row 8 locates the class in the object model — it is an instance of Class, which is a Module, and a Class.new with no argument extends Object. Row 9 is the timing consequence for the inherited hook: Class.new(Animal) fired it with a nameless subclass, class Kitten < Animal with a named one. Row 10: the block is a closure, so define_method inside it can use factor. Row 11 draws the limits — Class.new(Class) is refused (there are no metaclasses in Ruby; singleton classes take that role) and a module cannot be a superclass. Row 12: a constant name must be capitalised, so const_set(:lower, ...) is a NameError. Row 13: remove_const takes a name away again (it is private — hence send) and returns the class, which lives on unnamed. Row 14: Class.new inside a method returns a different class each time, each one anonymous, which is how test suites make throwaway classes. Row 15 is a difference the Python twin fills in.

Compared with Python

Python's three-argument type(name, bases, namespace) is Class.new with the name supplied up front, so rows 2–4 collapse: a class is named at birth, an empty string is an acceptable name, assignment to a variable changes nothing, and __name__ is simply a writable attribute. Row 5 shows the other half of the naming model: __qualname__ and __module__ are set when the class is made and do not follow it into a module you setattr it onto — Python classes are named by where they were defined, Ruby classes by where they were first assigned. Row 6: a mixin is a class in the base tuple, and a namespace is a types.ModuleType object, so Ruby's one Module concept is two Python things. Row 7: collections.namedtuple and dataclasses.make_dataclass are the twins of Struct.new and Data.define. Row 8: type(k) is type — the class of a class is a class, and isinstance(k, type) is how you ask "is this a class?". Row 9: __init_subclass__ always sees a name. Row 11 is the deepest divergence: type("Meta", (type,), {}) is allowed, and it is a metaclass — Python lets you subclass the class of classes and customise class creation there, where Ruby refuses Class.new(Class) and uses singleton classes and hooks instead. Row 12: any string is a legal __name__. Row 13: a class in a module is an attribute, so hasattr/delattr on the module object replace const_defined?/remove_const. Row 14: a class statement inside a function also makes a fresh class per call, and its __qualname__ records the nesting as make_class.<locals>.Made. Row 15: types.new_class and the four-argument type(..., tag="y") deliver keyword options to __init_subclass__, which Ruby's Class.new cannot do — a Ruby class takes its options by calling a class method after it exists.

Verified output of classes_at_runtime_py.py — regenerated by tools/run_examples.py, never hand-typed.

 1. k = type("Dog", (Animal,), {"speak": speak})   k().speak() = 'woof'
 2. k.__name__ -- named at birth                   'Dog'
    type("", (), {}).__name__ -- even an empty name ''
 3. Dog = k; k.__name__                            'Dog'
    k.__name__ = "Renamed"; k.__name__             'Renamed'
 4. Hound = Dog; Hound.__name__                    'Dog'
 5. setattr(zoo, 'Cat', cat); cat.__qualname__     'Cat' (unchanged), __module__ = '__main__'
    getattr(zoo, "Cat") is cat                     True
 6. type('Dog2', (Greeting, Animal), {})().hi()    'hi', mixin name = 'Greeting'
    [c.__name__ for c in Dog2.__mro__[:2]]         ['Dog2', 'Greeting']
 7. namedtuple("Point", "x y")(1, 2)               Point(x=1, y=2)
    make_dataclass("Coord", ["lat", "lng"])(1, 2)  Coord(lat=1, lng=2)
 8. type(k) / k.__bases__ / type('A', (), {}).__bases__ type / ('Animal',) / ('object',)
    isinstance(k, type)                            True
 9. __init_subclass__ saw, in order (k, Dog2, Kitten) ['Dog', 'Dog2', 'Kitten']
10. type('T', (), {'triple': triple})().triple(3)  9
11. Meta = type('Meta', (type,), {}) -- allowed    mro = ['Meta', 'type', 'object']
    a Python module is an instance of ModuleType   isinstance(zoo, types.ModuleType) = True
12. type('bad name', (), {}).__name__ -- any str   'bad name'
13. hasattr(module, "Hound")                       True
    delattr(module, "Hound") -> then hasattr       False
14. make_class() is make_class() / __qualname__    False / 'make_class.<locals>.Made'
15. types.new_class('T', (Base,), {'tag': 'x'}).tag 'x'
    type('T2', (Base,), {}, tag='y').tag           'y'

Try it

  1. Generate Celsius, Fahrenheit and Kelvin classes from a Hash of name → conversion lambda: each is a Class.new(Temperature) whose to_kelvin closes over its own lambda, named with Object.const_set. Print a name and superclass, three conversions, Temperature.subclasses, and a lookup by const_get.
  2. Write make_enum(:Colour, %w[red green blue]) that builds an anonymous class, gives it one frozen instance per value as a constant (Colour::RED), a values class method, and an inspect that prints #<Colour red> — then show that Colour::RED.equal?(Colour.values.first) and that the class's name is "Colour" only after you assign it.
Solution to 1

classes_at_runtime_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# classes_at_runtime_kata_rb.rb — temperature classes generated from a table.

class Temperature
  attr_reader :value
  def initialize(value) = @value = value
end

TO_KELVIN = {
  Celsius:    ->(v) { v + 273.15 },
  Fahrenheit: ->(v) { (v + 459.67) * 5 / 9 },
  Kelvin:     ->(v) { v },
}

TO_KELVIN.each do |name, convert|
  klass = Class.new(Temperature) do
    define_method(:to_kelvin) { convert.call(value).round(2) }   # closes over this unit's lambda
  end
  Object.const_set(name, klass)                                  # gives it its name
end

puts "1. Celsius.name / superclass         -> #{Celsius.name} / #{Celsius.superclass}"
puts "2. Celsius.new(100).to_kelvin        -> #{Celsius.new(100).to_kelvin}"
puts "3. Fahrenheit.new(212).to_kelvin     -> #{Fahrenheit.new(212).to_kelvin}"
puts "4. Kelvin.new(373.15).to_kelvin      -> #{Kelvin.new(373.15).to_kelvin}"
puts "5. Temperature.subclasses names      -> #{Temperature.subclasses.map(&:name).sort.inspect}"
puts "6. Object.const_get(\"Celsius\").new(0).to_kelvin -> #{Object.const_get("Celsius").new(0).to_kelvin}"

Verified output of classes_at_runtime_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

1. Celsius.name / superclass         -> Celsius / Temperature
2. Celsius.new(100).to_kelvin        -> 373.15
3. Fahrenheit.new(212).to_kelvin     -> 373.15
4. Kelvin.new(373.15).to_kelvin      -> 373.15
5. Temperature.subclasses names      -> ["Celsius", "Fahrenheit", "Kelvin"]
6. Object.const_get("Celsius").new(0).to_kelvin -> 273.15

See also