Several languages ship built-in help for making one object stand in for another: Kotlin's `by` clause, Scala 3's `export`, Go struct embedding, Ruby's Forwardable/SimpleDelegator, and Python's `__getattr__`. Compare what each of them actually generates, and what each one cannot do.
answer
- all five generate forwarding, never delegation
- Kotlin `by`: interface required, fixed at construction
- Scala 3 `export`: forwarders without subtyping
- Go embedding: name promotion, no interception hook
- `__getattr__` skipped by len()/[] — dunders on the type
basics
~20 sAll of them generate forwarding, not delegation. Kotlin by, Scala 3 export and Go embedding emit compile-time stubs that call the inner object; Ruby's Forwardable metaprograms named methods; Python's __getattr__ forwards at run time but is skipped by implicit special-method lookup.
solid answer
~50 sThey differ on three axes: whitelist vs whole surface, when the target is bound, and whether there is an interception seam. - **Kotlin** `class L(d: Repo) : Repo by d` emits one stub per interface member; the delegate is stored at construction and can never be swapped, and the interface is mandatory. - **Scala 3** `export inner.*` generates the same forwarders but as ordinary members, with rename/exclude, and it does **not** make you a subtype — delegation and subtyping are separate declarations, unlike Kotlin's `by`. - **Go** embedding promotes names at compile time and carries interface satisfaction along, but offers no hook to intercept a promoted call. - **Ruby** `def_delegators` whitelists methods at class-definition time; `SimpleDelegator` forwards via `method_missing` and can retarget at run time with `__setobj__`. - **Python** `__getattr__` forwards everything, including typos, but `len(w)` and `w[i]` look the dunder up on the type and never reach it.
code
python · 10 linesclass Wrap:
def __init__(self, inner):
self._inner = inner
def __getattr__(self, name):
return getattr(self._inner, name)
w = Wrap([1, 2, 3])
w.append(4) # forwarded, works
w.__len__() # 4 - explicit call is forwarded
len(w) # TypeError: object of type 'Wrap' has no len()go deeper
Know that these features write the boring pass-through methods for you, and that they call the inner object rather than making it aware of you.
Be able to place each mechanism on the whitelist-vs-whole-surface and compile-time-vs-run-time axes, and to name the Python special-method-lookup trap.
Choose deliberately: pick the dynamic mechanisms when you need one interception seam or a swappable target, the static ones when you want the compiler to keep the surface in sync.
Reason about the cost across a codebase: catch-all forwarding hides surface drift from review and tooling, static generation makes every upstream member addition a visible recompile.
## The problem these features solve When one object stands in for another — a wrapper that adds logging, a decorator that adds caching, an adapter that renames — you have to re-expose the inner object's methods. Written by hand that is one stub per member: `fun m(a) = inner.m(a)`. For a twenty-member interface that is twenty bodies with no logic in them, which must be revisited every time the interface grows. Several languages ship a feature that writes those stubs for you. The single most important fact about all of them: they generate **forwarding**. Each produces (or simulates at run time) a call `inner.m(...)`. None of them tells the inner object that it is being wrapped, and none re-binds the inner object's notion of self. ## Kotlin: the `by` clause `class Logged(private val d: Repo) : Repo by d` makes the compiler emit an implementation of every member of `Repo` that calls `d.m(...)`. Constraints worth naming: the delegated type must be an **interface**; the delegate expression is evaluated once at construction and stored in a hidden field, so there is no run-time swap; and any member you override in `Logged` is invisible to `d` — code inside `d` calling `m()` runs `d`'s own `m`. ## Scala 3: `export` `export inner.*` generates forwarders as ordinary members of the enclosing class or object, and you can select (`export inner.{read, write}`), rename, or exclude members. Nothing about `export` makes your class a subtype of the exported type. That is the sharpest contrast with Kotlin: in Kotlin, `by` is simultaneously the subtyping declaration and the forwarding declaration, so you get the whole interface or nothing; in Scala 3 you decide those two things separately. ## Go: struct embedding `type Wrap struct { Inner }` promotes `Inner`'s methods and fields onto `Wrap` by name at compile time. Promotion is shallowest-wins: a method declared directly on `Wrap` shadows the promoted one, and two embedded types offering the same name at the same depth are not an error until a call site selects it. Promotion also carries interface satisfaction — `Wrap` satisfies whatever `Inner` satisfies, which is why embedding is Go's mixin idiom. The receiver of a promoted method is still the `Inner` value, and Go supplies no interception hook at all. ## Ruby: Forwardable and SimpleDelegator `extend Forwardable; def_delegators :@inner, :read, :write` metaprograms named methods when the class body runs — an explicit whitelist, compiled once. `SimpleDelegator` is the catch-all sibling: it forwards unknown messages through `method_missing`, and it lets you replace the target at run time with `__setobj__`. That retargeting is something none of the compile-time features can do. ## Python: `__getattr__` `def __getattr__(self, name): return getattr(self._inner, name)` is the most permissive mechanism on the list. It forwards everything the wrapper does not define, including names that are typos, and it works on objects whose surface is not known statically. It also has a trap that catches people repeatedly: **implicit special-method invocation looks the dunder up on the type, not the instance**, so `len(w)`, `w[i]`, `with w`, `w + y` and iteration never reach `__getattr__`. `w.__len__()` works; `len(w)` raises `TypeError`. Wrapping anything protocol-shaped in Python means writing the dunders out explicitly. ## The three axes to remember 1. **Whitelist vs whole surface.** Scala's `export` and Ruby's `def_delegators` name members. Kotlin's `by` and Go embedding take a declared type's entire surface. `__getattr__` and `method_missing` take everything, known or not. 2. **When the target is bound.** Compile time for Kotlin, Scala and Go; class-definition time for `def_delegators`; per call for `__getattr__` and `SimpleDelegator` — and only that last group can swap the target on a live object. 3. **Whether there is a seam.** Dynamic mechanisms give you exactly one place to add a cross-cutting concern (timing, retries, auditing) for every member. The static generators give you none: to instrument one member you must write it by hand and let the generator cover the rest. ## What none of them buy you No mechanism here makes the inner object cooperate with the outer one. The inner object's self-calls, the identity it hands to callbacks, and the type it reports all remain the inner object's. That is the same for Kotlin, Scala, Go, Ruby's Forwardable and Python's `__getattr__` — which is why the language-level support removes typing, not the design consequences of wrapping.
- Which of these mechanisms can change the object being forwarded to after construction, and why can the others not?Only the run-time ones: Ruby's SimpleDelegator via `__setobj__`, and any Python `__getattr__` wrapper that reads a mutable attribute. Kotlin's `by` stores the delegate in a hidden field created at construction with no setter, Scala's `export` bakes the forwarders against a specific path, and Go's promotion is a compile-time name resolution. If you need swapping in those languages you must write the field and the stubs yourself.
- Why does wrapping a Python object that supports `len()`, indexing or `with` require more work than wrapping a plain service object?Because CPython looks special methods up on the type object for implicit invocations, bypassing both `__getattr__` and instance attributes. A `__getattr__`-based wrapper therefore forwards `read()` fine but fails `len(w)`, `w[0]` and `with w`. You must declare `__len__`, `__getitem__`, `__enter__`/`__exit__` and friends explicitly on the wrapper, which is why library wrappers there are much longer than the one-line `__getattr__` suggests.
saying these in an interview costs you the question
- Calling Kotlin's `by` or Go embedding 'delegation' in the prototype-language sense — they never re-bind the callee's self.
- Assuming a Python `__getattr__` wrapper is a drop-in replacement, then being surprised that `len()` or `with` fails on it.
- Believing Go embedding lets the outer type override the inner type's behaviour for the inner type's own calls.
- Thinking Scala 3's `export` makes the enclosing class a subtype of the exported type.