skip to content

In Ruby, how do dup and clone differ for singleton methods and frozen state, and what does clone(freeze:) change?

level: middleimportance: must knowfreq 72%

answer

  1. both copies are shallow
  2. clone copies the singleton class
  3. dup drops extend-ed modules
  4. dup of a frozen object: unfrozen
  5. freeze: true or false overrides

basics

~20 s

Both make shallow copies. clone also copies the singleton class (singleton methods and extended modules) and the frozen state; dup copies neither, so it returns a plain, unfrozen copy. clone(freeze: true) or clone(freeze: false) sets the copy's frozen state explicitly.

solid answer

~40 s

`dup` and `clone` both allocate a new object of the same class, copy the instance variables (references, not the objects behind them) and call a copy hook. `clone` additionally copies the **singleton class**, so methods defined with `def obj.x` or added with `extend` survive, and by default it copies the **frozen state**. `dup` drops both, which is why `frozen_thing.dup` is the usual way to get an editable copy. Since Ruby 3.0, `clone(freeze: true)` returns a frozen copy even of an unfrozen receiver and `clone(freeze: false)` an unfrozen one; any other value raises `ArgumentError`. Integers, Floats, Symbols and similar special values return themselves from both methods, and `42.clone(freeze: false)` raises `ArgumentError` ("can't unfreeze Integer").

code

ruby · 17 lines
ruby
module Watermarked
  def watermark = "DRAFT"
end

template = Object.new
template.extend(Watermarked)
def template.preview = "preview"
template.freeze

template.dup.frozen?                         # => false
template.dup.respond_to?(:watermark)         # => false
template.clone.frozen?                       # => true
template.clone.preview                       # => "preview"
template.clone(freeze: false).frozen?        # => false
Object.new.clone(freeze: true).frozen?       # => true (Ruby 3.0+)
5.dup.equal?(5)                              # => true
5.clone(freeze: false)                       # ArgumentError: can't unfreeze Integer

go deeper

for a junior

Recall that dup and clone both copy shallowly, and that only clone keeps singleton methods and the frozen state.

for a middle

Explain the table: which of singleton methods, extended modules and frozen state each method copies, and what clone(freeze: true) and freeze: false override.

for a senior

Choose dup for editable drafts of frozen masters and clone for exact replicas, and account for special values that return themselves from both.

for a principal

Decide whether objects in a codebase should ever be decorated per instance, since singleton methods make dup and clone diverge in ways reviewers miss.

## One shared core `Kernel#dup` and `Kernel#clone` start the same way. For an ordinary object each one: 1. **allocates** a new, empty object of the receiver's class (it does not call `initialize`); 2. **copies the instance variables**, so the copy's `@sections` points at the very same Array as the original's; 3. calls a **copy hook** on the new object (`initialize_dup` for `dup`, `initialize_clone` for `clone`, and both of those call `initialize_copy`). Both are therefore **shallow**: nested objects are shared, not copied. The differences are in what else travels with the copy. ## What clone keeps and dup drops | Property of the original | `dup` | `clone` | `clone(freeze: false)` | `clone(freeze: true)` | |---|---|---|---|---| | Instance variables (as references) | copied | copied | copied | copied | | Singleton methods (`def obj.preview`) | dropped | kept | kept | kept | | Modules added with `extend` | dropped | kept | kept | kept | | Frozen state | never frozen | same as original | not frozen | frozen | So `clone` means "an exact replica of this object", and `dup` means "a fresh instance of the same class with the same state". The Ruby documentation's own `dup` example extends an object with a module and shows that the `dup` copy raises `NoMethodError` for the module's method while the `clone` copy still responds. ## A document template example Imagine a frozen master template that was decorated at runtime: - `MASTER = DocumentTemplate.new("Invoice", sections)` - `MASTER.extend(Watermarked)` adds a `watermark` method to that one object - `MASTER.freeze` protects it Then: - `MASTER.dup` is **unfrozen** and has **no** `watermark` method: a clean working draft. - `MASTER.clone` is **frozen** and still has `watermark`: an identical, read-only replica. - `MASTER.clone(freeze: false)` keeps `watermark` but is editable. ## clone(freeze:) The `freeze:` keyword accepts only `true`, `false` or `nil` (the default); anything else raises `ArgumentError` ("unexpected value for freeze"). Two behaviours are worth stating precisely: - `clone(freeze: false)` returns an unfrozen copy of a frozen object while keeping singleton methods, which `dup` cannot do. - Since **Ruby 3.0**, `clone(freeze: true)` returns a frozen copy even when the receiver is not frozen, and the `freeze:` value is passed on to `initialize_clone`, so a class that overrides `initialize_clone` must accept that keyword. ## Values that are never copied Some objects are immutable and unique by nature: `nil`, `true`, `false`, Integers, Floats, Symbols, Rationals and Complex numbers. For them: - `dup` and `clone` simply **return the receiver**, so `5.dup.equal?(5)` is `true`. - `clone(freeze: false)` raises `ArgumentError`, for example `can't unfreeze Integer`, because the value is always frozen. ## Traps interviewers probe - **"clone is the deep one."** Neither method copies nested objects; the difference is singleton class and frozen state only. - **"dup keeps the object frozen."** It never does, which is exactly why `dup` is the idiomatic way to get an editable copy of a frozen value. - **Singleton methods added in tests.** A spec that stubs a method on one object by defining a singleton method sees it vanish on a `dup` copy but survive on a `clone`, which can explain a puzzling test failure. - **Copy hooks differ.** `dup` calls `initialize_dup` and `clone` calls `initialize_clone`; both default to `initialize_copy`, so a class can make the two copies behave differently when it needs to. ## Choosing between them - Use **`dup`** when you want an editable working copy, especially of something frozen, and do not want per-object decorations to leak into it. - Use **`clone`** when the copy must behave exactly like the original, including singleton methods and frozen state. - Reach for **`clone(freeze: false)`** in the rare case where you need both the decorations and mutability. Whichever you choose, remember that nested objects are still shared. If the copy will mutate a nested Array or Hash, copy that level too, typically in `initialize_copy`.

  • When would you use `clone(freeze: false)` instead of `dup`?
    When the copy must keep the original's singleton methods or extended modules but also be editable. `dup` gives an unfrozen copy but drops those per-object decorations, while plain `clone` keeps them but also keeps the frozen state. `clone(freeze: false)` is the only one of the three that keeps the decorations and returns an unfrozen object.
  • What do `dup` and `clone` return for a Symbol or an Integer?
    The receiver itself. These values are immutable and have a single identity, so copying would be pointless: `:draft.dup.equal?(:draft)` is true. Both methods return the object, and asking for `clone(freeze: false)` raises ArgumentError because such values are always frozen.

saying these in an interview costs you the question

  • dup makes a deep copy, while clone makes a shallow one.
  • dup keeps singleton methods and modules added with extend.
  • The dup of a frozen object is frozen too.
  • clone always returns a frozen copy.
  • Integer#dup raises TypeError because numbers cannot be copied.