Type Systems Explained: Static vs Dynamic, Strong vs Weak
Every programming language makes a decision about when it checks whether you are using a value correctly — at compile time or at runtime — and how aggressively it enforces those checks. These choices define the language's type system, which in turn shapes the kinds of bugs you encounter, the tooling you can build, and the speed at which you can move.
Published June 30, 2026The terms "static," "dynamic," "strong," and "weak" are thrown around constantly but are frequently conflated. They describe two independent axes: when types are checked (static vs dynamic) and how strictly type mismatches are enforced (strong vs weak). A language can sit anywhere on these two axes independently.
Static typing: errors at compile time
In a statically typed language, every variable has a known type before the program runs. The compiler uses these annotations to verify that operations are valid — that you are not trying to call .upper() on an integer, for instance — and rejects the program if a type constraint is violated.
// Java: the type of every variable is declared
String name = "Alice";
int age = 30;
// Compiler error: cannot assign int to String variable
String name2 = 42; // error: incompatible types
The payoff is a class of bugs that simply cannot exist at runtime. The cost is verbosity and a compile step that adds friction to the edit-run cycle. Modern static languages reduce the verbosity substantially through type inference, where the compiler deduces the type from context rather than requiring an explicit annotation on every line.
// Rust: types inferred from the assigned value
let name = "Alice"; // inferred: &str
let scores = vec![90, 85]; // inferred: Vec<i32>
// The compiler still knows the types; it just does not require you to write them
scores.push("oops"); // compile error: expected i32, found &str
Dynamic typing: errors at runtime
In a dynamically typed language, types are associated with values rather than variables. A variable can hold an integer, then later a string, then a list. Type checks happen at runtime, when an operation is actually executed.
# Python: no type annotation required
x = 42
x = "now I'm a string" # valid; x holds a new value of a different type
def double(n):
return n * 2
double(5) # returns 10
double("hi") # returns "hihi" -- str * int is defined in Python
double([1, 2]) # returns [1, 2, 1, 2] -- list * int is also defined
The advantage is speed of iteration: you can prototype, reshape data structures, and run a script without a compilation step. The disadvantage is that type errors only surface when the problematic code path is actually executed, which in production can mean a bug reaches users before it is caught.
Strong vs weak: how far the runtime bends
Strong and weak typing describe how a language handles operations between mismatched types. A strongly typed language raises an error; a weakly typed language attempts an implicit conversion (coercion).
# Python is dynamically AND strongly typed
"3" + 3 # TypeError: can only concatenate str (not "int") to str
// JavaScript is dynamically AND weakly typed
"3" + 3 // "33" -- number coerced to string silently
"3" - 3 // 0 -- string coerced to number silently
[] + {} // "[object Object]"
{} + [] // 0
JavaScript's implicit coercions are the source of a substantial category of subtle bugs. When "3" + 3 silently produces the string "33" instead of raising an error, an incorrect assumption about a value's type produces a result that is wrong but not immediately obviously wrong.
Duck typing
Dynamic languages often rely on duck typing: if an object has the method or attribute you need, you can use it, regardless of its declared type. The name comes from the phrase "if it walks like a duck and quacks like a duck, it is a duck."
def process(file_like):
data = file_like.read() # works for open files, io.StringIO, BytesIO,
return data # any object with a .read() method
import io
process(open("data.txt"))
process(io.StringIO("inline content"))
Duck typing enables a kind of structural polymorphism without inheritance hierarchies. The trade-off is that the contract is implicit; nothing in the code tells a reader exactly what file_like must provide.
Gradual typing: TypeScript and type hints
Gradual typing systems let you add type annotations incrementally to a dynamic codebase. TypeScript compiles to JavaScript but adds a static type layer. Python's typing module lets you annotate functions for static analysis tools like mypy without changing runtime behavior.
// TypeScript: static types over JavaScript
function greet(name: string): string {
return "Hello, " + name;
}
greet("Alice"); // fine
greet(42); // TypeScript compiler error: Argument of type 'number'
// is not assignable to parameter of type 'string'
TypeScript does not change JavaScript at runtime; the type annotations are erased when the code is compiled. The value is entirely in the development and build phase: editors can offer accurate autocomplete, and a class of type errors is caught before deployment.
Choosing a type system
The practical trade-off is between correctness guarantees and iteration speed. Static typing pays off most in large codebases maintained by many developers over years, where the compiler acts as a continuously running correctness check. Dynamic typing pays off most in small scripts, rapid prototyping, and domains where the data shapes are irregular enough that static types would add more annotation overhead than they catch bugs.
The trend in the industry is toward adding optional static typing to dynamic languages — TypeScript, Python type hints, Flow — rather than abandoning either model entirely. This gives teams the freedom to prototype without annotations while adding rigor progressively as code stabilizes.