Once I asked myself a question that stopped me for a second: "Why don't we just make everything dynamic? Then we never have to worry about types at all."
I knew var, object, and dynamic were different, I used var constantly and avoided dynamic out of some vague instinct...but I couldn't actually explain why in a way that would convince me.
They Look Similar. They Are Not the Same Thing.
All three let you write code without explicitly naming a concrete type on the left-hand side, which is exactly why they get confused:
var a = "hello";
object b = "hello";
dynamic c = "hello";All three compile. All three run. All three currently hold a string. But they behave completely differently the moment you do anything interesting with them, because each one represents a different stage at which C# figures out the type.
var: the type is decided at compile time, by the compiler, based on what's on the right-hand side.
object: the type is genuinely object at compile time, anything more specific is hidden until you cast it back.
dynamic: type checking is deferred to runtime entirely. The compiler steps back and just trusts you.
var
This is the one people misunderstand most. var is not "no type." It's not JavaScript's var. The compiler looks at the right-hand side, figures out the concrete type, and locks it in permanently at compile time.
var name = "Alice"; // compiler infers string
var count = 5; // compiler infers int
var product = GetProduct(); // compiler infers whatever GetProduct() returns
name = 42; // ERROR — name is a string, always was, always will beThis matters for performance too: since the type is fully known at compile time, there's zero runtime overhead.
object: Everything's Base Type, With a Boxing Tax
object is the ancestor of every type in C#, every class, every struct, everything. So you can store anything in an object variable. The catch is that once it's in there, the compiler only knows about the object members (ToString(), Equals(), GetHashCode(), GetType()), nothing else.
object o = "hello";
int length = o.Length; // ERROR — object has no Length propertyEven though the runtime value is clearly a string, the compiler refuses to let you call .Length on it, because as far as compile time type checking is concerned, o is just an object. To get the string back, you need an explicit cast:
object o = "hello";
string s = (string)o;
int length = s.Length; // fine nowThe Boxing Problem
When you put a value type into an object, C# has to "box" it, wrap the value type in a heap-allocated object so it can be referred to by reference.
int number = 42;
object boxed = number; // BOXING: number gets copied onto the heap
int unboxed = (int)boxed; // UNBOXING: copied back onto the stackBoxing isn't free. Every box is a heap allocation, which means garbage collection pressure.
dynamic
dynamic looks like var but does the opposite thing. Instead of the compiler pinning down a type early, it skips type checking altogether and figures things out at runtime, using something called the Dynamic Language Runtime (DLR).
dynamic thing = "hello";
Console.WriteLine(thing.Length); // works, resolved at runtime
thing = 42;
Console.WriteLine(thing.Length); // compiles fine... then throws at runtime!Both lines compile without complaint, because the compiler isn't checking anything about thing's members, it just trusts that whatever method or property you call will exist when the program actually runs.
dynamic doesn't make types disappear. It just moves the moment you discover a type error from "while you're typing" to "while a customer is using the app."
Where dynamic genuinely earns its keep
It has real, legitimate uses:
Reflection scenarios where you're already working with unknown shapes at runtime
Working with dynamic data formats like JSON before you've modeled it into a class
Side-by-Side, So It Actually Sticks
var | object | dynamic | |
|---|---|---|---|
Type resolved | Compile time (inferred) | Compile time (as object) | Runtime |
Requires initializer | Yes | No | No |
Type errors caught | Compile time | Compile time (via cast) | Runtime only |
Value types get boxed? | No (retains real type) | Yes, when assigned | Yes, when needed |
Performance | No overhead | Boxing overhead for value types | Slower (DLR resolution each call) |
So Which One Do You Actually Reach For?
Use var almost by default for local variables, especially when the type is obvious from the right-hand side (
var list = new List<Product>();) or painfully long (var result = someDictionary.GroupBy(x => x.Key).ToDictionary(...)).Use object when you genuinely need a variable that can hold any type and you're okay explicitly casting it back later.
Use dynamic sparingly and deliberately: reflection, dynamically-shaped external data, or scripting-style scenarios. If you find yourself using dynamic just to avoid defining a proper class or interface, that's usually a sign to go define the class or interface instead.

Join the conversation! Your thoughts help the community grow.