Type Systems: Static, Dynamic, Strong, Weak - What These Actually Mean


“Python is dynamically typed.” “C++ is strongly typed.” “JavaScript is weakly typed.” These claims are thrown around confidently, sometimes accurately, often imprecisely. The terms “strong” and “weak” in particular are used inconsistently enough to cause real confusion.

Let’s be precise.

Static vs Dynamic Typing

This axis is about when type checking happens.

Static typing: types are checked at compile time, before the program runs. If a type error exists in code that’s never actually executed, the compiler still catches it. The compiler needs to know the type of every expression.

// Java - static typing
String name = "Alice";
int length = name.length();  // Fine
int doubled = name * 2;      // Compile error - can't multiply String by int

Dynamic typing: types are checked at runtime, when an operation is actually performed. The same code might work for some inputs and fail for others, depending on what’s passed in.

# Python - dynamic typing
def double(x):
    return x * 2

double(5)      # Works: 10
double("hi")   # Also works: "hihi" (string repetition)
double([1,2])  # Works: [1, 2, 1, 2]
double({})     # TypeError at runtime - can't multiply dict by int

Static typing catches a class of errors before deployment. Dynamic typing is more flexible - you can write polymorphic code without explicit interfaces. Neither is universally better; the tradeoff is compile-time safety vs flexibility.

Statically typed languages: C, C++, Java, Go, Rust, TypeScript, Swift. Dynamically typed languages: Python, JavaScript (pre-TypeScript), Ruby, PHP.

Strong vs Weak Typing

This axis is about what the language does with type mismatches. Unfortunately, “strong” and “weak” are not standardized terms - different sources define them differently. Here’s the most useful definition:

Strong typing: the language refuses to perform operations on incompatible types. It requires explicit conversion.

Weak typing: the language implicitly coerces values between types to make operations work. Type errors are “solved” by guessing what you meant.

The clearest example is JavaScript (weak) vs Python (strong):

// JavaScript - weak typing
"5" + 3        // "53" (string + number = string concatenation)
"5" - 3        // 2    (string - number = numeric subtraction... because why not)
[] + {}        // "[object Object]"
{} + []        // 0    (if statement, not expression)
true + true    // 2
# Python - strong typing
"5" + 3        # TypeError: can only concatenate str (not "int") to str
"5" - 3        # TypeError: unsupported operand type(s)

Python refuses and tells you what went wrong. JavaScript invents an answer that might not be what you wanted. Python is strongly typed. JavaScript is weakly typed.

Note that strong/weak is independent of static/dynamic. Python is strongly and dynamically typed. TypeScript is strongly and statically typed. C is statically typed but relatively weak (implicit int/pointer conversions, no bounds checking).

The Four Quadrants in Practice

StaticDynamic
StrongJava, Go, Rust, TypeScriptPython, Ruby
WeakC (somewhat), C++ (somewhat)JavaScript, PHP (older versions)

C and C++ are in a gray zone - they’re statically typed but allow implicit conversions between numeric types and pointer-to-integer conversions that most people would call “weakly typed.”

PHP has moved significantly toward stronger typing in recent versions, especially with strict mode and typed properties.

Structural vs Nominal Typing

A third axis, less commonly discussed but important: how the language determines type compatibility.

Nominal typing: a type is compatible with another only if it explicitly declares so (extends, implements). Two types with identical structure are not compatible unless they have an explicit relationship.

// Java - nominal typing
interface Printable { void print(); }

class Document implements Printable {
    public void print() { /* ... */ }
}

class Image {
    public void print() { /* ... */ }  // Same method, but doesn't implement Printable
}

void printAll(List<Printable> items) { /* ... */ }
// Image cannot be passed here even though it has a print() method

Structural typing (also called “duck typing” in dynamic contexts): a type is compatible with another if it has all the required structure. No explicit declaration needed.

// TypeScript - structural typing
interface Printable { print(): void; }

class Image {
    print() { /* ... */ }  // No explicit interface declaration
}

function printAll(items: Printable[]) { /* ... */ }
printAll([new Image()]);  // Works - Image has print()

Go’s interfaces are structural. TypeScript’s types are structural. Java’s interfaces are nominal. This has practical consequences: Go code can satisfy interfaces defined in libraries you don’t control; Java code requires changing the implementing class.

Type Inference

Modern statically typed languages infer types rather than requiring explicit annotations everywhere:

// TypeScript - type inference
const x = 42;          // TypeScript infers x: number
const name = "Alice";  // TypeScript infers name: string
const arr = [1, 2, 3]; // TypeScript infers arr: number[]

// You don't need to write:
const x: number = 42;
// Go - type inference
x := 42       // inferred as int
name := "Bob" // inferred as string

Rust’s type inference is particularly powerful, inferring types through complex generic bounds. This gives you the safety of static typing with much of the ergonomics of dynamic typing.

What This Means for Everyday Work

When choosing a language: static typing is significantly more valuable in large codebases and large teams. The compiler acts as documentation and catches mistakes at scale. For small scripts and prototypes, dynamic typing’s flexibility is attractive.

When debugging: a TypeError in Python (“can’t multiply sequence by non-int of type ‘float’”) tells you exactly what happened. A JavaScript type coercion gives you a wrong answer without complaint. Debugging wrong answers is harder than debugging errors.

When reading code: strongly typed code is often self-documenting. A function signature def process_order(order: Order) -> Receipt tells you more than def process_order(order).

When using TypeScript: understand that TypeScript is structurally typed. You don’t need to explicitly implement interfaces - any object with the right shape satisfies an interface. This is different from Java/C# and trips up developers coming from those languages.

The terms exist to describe real differences in how languages behave. Using them precisely helps you reason about what guarantees a language actually provides - and where you need to be careful despite those guarantees.



Read more