skip to content

Some runtimes let a running program manufacture a brand-new class: Python's built-in `type(name, bases, namespace)`, Ruby's `Class.new`, and the Objective-C runtime's `objc_allocateClassPair` + `objc_registerClassPair` pair. Several also fire a hook the instant a subtype appears — Python's `__init_subclass__`, Ruby's `inherited`. Other platforms fix the set of types before the program starts: Rust has no runtime class construction, Go's `reflect.StructOf` can fabricate a struct shape but cannot attach methods to it, and a GraalVM native-image binary only contains types the build could see. What does runtime type construction buy a framework author, what does closing the type set buy in return, and how do you decide which cost your system pays?

level: principalimportance: nice to knowfreq 22%

answer

  1. when is the type set final?
  2. construct a type ≠ emit machine code
  3. KVO builds NSKVONotifying_Foo at runtime
  4. reflect.StructOf: shapes yes, methods no
  5. native-image: reachability metadata or it was stripped

basics

~20 s

Runtimes that can build a class while running — Python's type(), Ruby's Class.new, Objective-C class pairs — let frameworks derive types from data and let subtypes self-register. Closing the type set instead buys ahead-of-time layout, dead-code stripping, and types you can actually see.

solid answer

~60 s

Frame it as **when the set of types is fixed**, and separate two things people conflate. **What construction buys:** types derived from data (an ORM turning a schema into a class per table), and definition-time hooks — Python's `__init_subclass__`, Ruby's `inherited` — so a plugin registry fills itself with no scan, no service file, no build step. Objective-C's KVO builds a notifying subclass per observed class at runtime; Core Data does the same for entities. **What closing buys:** a compiler can lay out every object, devirtualize, and strip everything unreached. Rust and Go rely on that; Go's `reflect.StructOf` deliberately stops short of new methods, so no *new behavior* appears. **The sharp distinction:** constructing a type is not the same as emitting code. Objective-C class construction is legal on iOS because methods are pointers to already-compiled functions; .NET's `Reflection.Emit` is banned there because it needs a JIT. GraalVM sits between: you may construct, but every reflectively-reached type needs build-time metadata or it was stripped. Decide by deployment: AOT/native/mobile targets pay for openness in configuration files and crashes at customer sites; long-lived JIT services can afford it.

code

text · 11 lines
text
# pseudocode
registry = []

base Plugin:
    on_subtype_defined(T):        # fires when any subtype is declared
        registry.append(T)

# ...and a type can be assembled at run time from a schema
fields = read_columns_from_database("users")
UserModel = make_class(name="UserModel", bases=[Plugin], members=fields)
# registry now contains UserModel; nothing scanned anything

go deeper

for a junior

Know that in some languages a class can be created while the program runs, and that in others every type exists before the program starts. Being able to name one example on each side is enough.

for a middle

Explain what it is used for — an ORM turning a database schema into classes, a base class that collects its own subtypes via a definition-time hook — and what the closed-world equivalent costs: a code generator, a registry file, or a startup scan.

for a senior

Own the operational consequence. Whole-program dead-code elimination and ahead-of-time compilation assume the type set is closed, so reflectively reached or runtime-built types need build-time metadata or they vanish from the binary, and the failure shows up in the packaged build rather than in tests.

for a principal

Separate type construction from code generation, since platforms restrict them independently, and drive the decision from the deployment target rather than the language. Note the convergence — open platforms bolt on static type descriptions, closed platforms bolt on code generation — and that picking one is picking which retrofit your team maintains.

## The question behind the question Every platform answers: **at what moment is the set of types in this program final?** Rust and Go answer *at build*. C# and Java answer *mostly at load*, with escape hatches. Python, Ruby, Smalltalk and the Objective-C runtime answer *never* — a new class can appear at any instant. Runtime type construction is the ability to create a type that no source file declared, complete with a name, a superclass, fields and methods, while the process is running. ## What construction buys **Types derived from data.** An object-relational mapper reads a schema and produces one class per table with one attribute per column. In Python this is a metaclass — a class whose instances are classes — running at import: Django's `ModelBase`, SQLAlchemy's declarative machinery, Pydantic's model metaclass all inspect the declared body and build the finished type from it. In Ruby, `Class.new(Base)` returns an anonymous class you can name by assigning it to a constant; `Struct.new` is the same trick in the standard library. On a closed platform the equivalent is a *build-time code generator* or *load-time bytecode synthesis*: an extra pipeline stage, an extra artifact, and a new way for generated code to drift from its source. **Registration without a scanner.** `__init_subclass__` and Ruby's `inherited` fire at the moment a subtype is defined. A plugin base class can therefore collect its own implementations as modules load — no configuration file, no classpath scan, no annotation processor. Closed-world equivalents all reintroduce the ceremony the hook removed: a service-provider descriptor, a startup scan of the deployed units, or a build-time registry. **Behavioral interception.** Apple's key-value observing does not patch your class; it *creates a subclass* (`NSKVONotifying_Foo`) with overridden setters and repoints the observed instance at it. Runtime class construction is the mechanism, and it is invisible to the code being observed. ## What closing the set buys **Layout and dispatch.** If no new type can appear, every field offset and every vtable is known at link time, generics can be monomorphized, and a call with one known implementor can be devirtualized outright. **Dead-code elimination.** Whole-program compilers and linkers delete what nothing reaches. This is exactly what an open world breaks: a method that only a runtime-built type will ever call looks unreachable, gets stripped, and the failure surfaces in production rather than in the build. **Comprehension and audit.** A member that exists only because a metaclass created it is invisible to grep, to IDE completion, and to a reviewer. Every open ecosystem eventually bolts on a *second, static* description to recover this — Python stub files, Ruby signature files, gradual type checkers — which is a static type system smuggled back in beside the dynamic one. **Go's deliberate half-step is instructive.** `reflect.StructOf` will fabricate a struct type at runtime, so serializers can build a shape from data. It will not give that type methods. Data shapes stay open; *behavior* stays closed, and the compiler keeps its assumptions. ## The distinction most candidates miss Constructing a type and generating machine code are different capabilities, and platforms restrict them separately. Objective-C class construction works fine on iOS, where runtime code generation is forbidden, because the methods you install are pointers to functions the AOT compiler already emitted — you are assembling metadata, not writing instructions. .NET's `Reflection.Emit` is unavailable under iOS and NativeAOT for the opposite reason: it emits new IL that would need a JIT. Interpreted platforms blur this only because their "code" is data the interpreter walks. GraalVM native-image is the clearest modern case of the tradeoff being priced. The JVM permits load-time class definition, and frameworks lean on it heavily — proxies, mocks, lambda call sites. Native-image performs closed-world analysis, so anything reached only reflectively or defined at runtime must be declared in build-time reachability metadata; otherwise the code was never compiled into the binary. Frameworks responded by moving work from runtime to build time, which is not a workaround but a genuine architectural shift. ## How to decide Ask what your deployment target is, not what your language allows. If you ship a native binary, a mobile app, a serverless function judged on cold start, or anything with a whole-program optimizer, openness is paid for in configuration files, fragile metadata, and failures that only appear in the packaged build. If you ship a long-lived JIT-compiled service where startup is amortized and a profiler can speculate on the shapes that actually occur, the same openness is nearly free and buys real leverage for framework authors. And note the convergence: open platforms retrofit static descriptions to get tooling back; closed platforms retrofit code generation to get dynamism back. Choosing a platform is largely choosing which retrofit your team maintains.

  • You said constructing a type is not the same as generating code. Where exactly does that line fall on a platform that forbids runtime code generation?
    Installing a method means storing a pointer to a function; if that function was already compiled ahead of time, no new instructions are produced and the platform is satisfied. That is why Objective-C can allocate and register a class pair on iOS while .NET's Reflection.Emit cannot run there — Emit produces new IL that would require a JIT. The practical test is whether the new type's behavior can be expressed entirely as combinations of already-compiled entry points.
  • A framework you depend on builds classes at load time. What changes when the team moves to an ahead-of-time native build?
    Closed-world analysis compiles only what it can reach, so types and members reached only reflectively or created at runtime are absent from the binary unless declared in build-time metadata. The usual answer is not to add configuration forever but to move the framework's work to build time — generate the types during compilation so they are ordinary, reachable code. Expect the failure mode to be a missing-class or missing-method error at first use in the packaged build, not at compile time.
  • Go can fabricate struct types at runtime with reflect.StructOf but refuses to attach methods. Why is that a coherent position rather than an unfinished feature?
    Fabricating a data shape leaves the dispatch story untouched: no new method sets means no new interface satisfactions the compiler did not see, so devirtualization, layout and linker dead-code elimination all remain sound. It gives serializers and decoders the one thing they actually need — a shape to fill — while keeping the behavioral type set closed. It is a deliberate split between open data and closed behavior.

A closed type set is a printed parts catalogue: fast to index, impossible to amend after printing. An open one is a wiki — a new page can appear at any moment, so no index built yesterday can be trusted, and you keep a librarian on staff.

saying these in an interview costs you the question

  • Claiming iOS or NativeAOT forbid creating classes at runtime in general — what they forbid is emitting new machine code; Objective-C class construction is legal there.
  • Treating runtime type construction as identical to modifying an existing class; construction adds a new type, and the two have different costs and different tooling consequences.
  • Asserting Go and Rust have no runtime type facilities at all, missing that Go's reflect.StructOf builds struct types but deliberately cannot attach methods.
  • Saying dynamic construction is simply slower, when the real price is loss of static reachability: dead-code elimination, IDE navigation and security audit all stop seeing the members.
  • Ranking one design as strictly better instead of tying the choice to the deployment target and who maintains the retrofit.

context