Static vs Dynamic Typing: Errors, Feedback Loops and Refactoring Flexibility

Follow one OrderLine model from an integer price to an INR Money value. See what Java catches during checking, what Python reveals during execution, and what both still need tests to prove.

KnowledgeGate Team

Exam prep & CS education

Updated 25 Sep 20265 min read

“Static is safer” and “dynamic is faster” are slogans, not a method for predicting what happens when a real data model changes. SKU PEN-07 has a unit price of 499 paise and quantity 3; the integer price then becomes Money("INR", 499) in the Java and Python examples. Static checking surfaces mistakes before execution, while dynamic checking waits for a path to run; tests and boundary validation address gaps left by both disciplines.

Static vs dynamic typing: separate the type axis from nearby ideas

Static programs check type obligations before execution, usually through a compiler or checker. Dynamic values have types; invalid operations are checked when reached. Dynamic does not mean typeless.

Question

Java example

Ordinary Python example

When an incompatible operation is reported

During checking or compilation

When execution reaches it

What a variable may hold across assignments

Values compatible with its static type

Values of different runtime types

Typical refactor feedback

Stale typed uses appear across compiled code

Stale uses appear on executed paths

What still needs tests?

Arithmetic and business rules

Arithmetic, shapes and business rules

Java inference remains static:

java
var total = 1497; // inferred as int

Python annotations alone are not runtime enforcement in the standard interpreter:

python
total: int = 1497
total = "pending"  # still runs

Compiled versus interpreted is a separate axis. Explore Coding & Skills for wider language learning.

Static and dynamic typing on the same OrderLine calculation

java
record OrderLine(String sku, int unitPrice, int quantity) {
    int subtotalPaise() {
        return unitPrice * quantity;
    }
}

var line = new OrderLine("PEN-07", 499, 3);
System.out.println(line.subtotalPaise());

With unitPrice = 499 and quantity = 3, 499 * 3 = 1497, so Java prints 1497. var saves repetition, but line remains an OrderLine.

Python:

python
def subtotal_paise(line):
    return line["unit_price"] * line["quantity"]

line = {"sku": "PEN-07", "unit_price": 499, "quantity": 3}
print(subtotal_paise(line))

Python also prints 1497. Compare the contracts under change, not syntax or speed. Classic Programs in C, Java and Python offers cross-language practice.

Java record fields and a Python dictionary both computing OrderLine 499 paise times quantity 3, reaching the same 1497 paise subtotal.

Static vs dynamic refactoring: change 499 into an INR Money value

Now the price must carry currency. Java adds record Money(String currency, int minorUnits) {} and changes OrderLine.unitPrice from int to Money.

Keep two stale lines. new OrderLine("PEN-07", 499, 3) supplies an int where Money is required. return unitPrice * quantity; multiplies Money by int. Checking reports both categories before execution. Repair them with new OrderLine("PEN-07", new Money("INR", 499), 3) and return unitPrice.minorUnits() * quantity;. Again, 499 * 3 = 1497.

In Python, set "unit_price": {"currency": "INR", "minor_units": 499} but keep subtotal_paise unchanged. Construction succeeds. The call reaches dictionary multiplication and raises TypeError: a dictionary cannot be multiplied by 3. Repair the body to line["unit_price"]["minor_units"] * line["quantity"]; it returns 1497.

Java exposes both stale uses during checking. Ordinary Python exposes its stale operation when that path runs. This makes neither all static refactors safe nor all dynamic defects production failures.

Timeline of the Money refactor: Java flags two stale uses before execution, Python raises TypeError on the path, both repaired to 1497.

Static typing catches incompatible uses, not incorrect business meaning

Java makes contracts visible: calls must supply Money, arithmetic must extract an integer, and methods expecting OrderLine reject unrelated objects. Object Oriented Technology Explained shows why a named value object carries more intent than a bare integer.

Static checking proves compatibility, not business meaning. new Money("USD", 499) is type-correct for an INR-only order. new OrderLine("PEN-07", new Money("INR", 499), -3) is also type-correct, and 499 * -3 = -1497. Currency policy and quantity > 0 need constructors, validators or domain types, plus tests.

A future JSON payload may contain "quantity": "3". Compilation cannot inspect it. Parsing and validation must reject the string before the typed core treats it as a quantity.

Dynamic typing buys shape flexibility but moves contract work elsewhere

One function returns 1497 for {"sku":"PEN-07", "unit_price":499, "quantity":3}. Unchanged, it returns 1000 for {"sku":"NOTE-02", "unit_price":250, "quantity":4, "campaign":"MONSOON"}. The extra field needs no new class because only price and quantity are read.

But {"sku":"PEN-07", "unit_price":499, "quantity":"3"} is subtle. Python evaluates 499 * "3" as a string of 499 copies of 3, not integer 1497. Adding 100 paise shipping later raises TypeError. Validation or assert isinstance(subtotal, int) catches this earlier. Practise with Python Output-Based Questions.

Dynamic shapes ease exploration, but contracts still need tests, validation, constructors, documentation or static analysis. Interfaces and generics also provide static flexibility.

Feedback loops depend on checkers, tests and executed paths

After the refactor, both implementations need this regression contract:

Case

Expected result

PEN-07, INR 499, quantity 3

Compute 1497

NOTE-02, INR 250, quantity 4

Compute 1000

String quantity "3"

Reject at the boundary

Legacy integer price 499

Reject after the Money schema change

Java checking finds stale uses across compiled code; tests verify arithmetic and rules. Python tests cover executed paths and shapes, making all four cases contractual.

Gradual typing moves Python feedback earlier. Define TypedDict shapes: Money has currency: str and minor_units: int; OrderLine has sku: str, unit_price: Money and quantity: int. A configured checker can flag the legacy integer price before execution. Untrusted input still needs runtime validation.

Run the checker or compiler first, then the four focused tests, then boundary validation, then one end-to-end path.

Static vs dynamic typing questions: trace the failure point, not the slogan

  1. Java var total = 1497 uses static type inference.

  2. Python total: int = 1497; total = "pending" runs in the ordinary interpreter.

  3. Stale Java Money * 3 fails checking before execution.

  4. A stale Python dictionary price multiplied by 3 fails when that line runs.

After 499 becomes Money("INR", 499), identify two stale Java uses, one stale Python operation and corrected 1497. Why is -3 producing -1497 a validation defect, not a type mismatch?

Situation

Useful design signal

Model shared by 4 modules

Early whole-codebase refactor feedback helps

One-file exploration whose fields change 3 times

Low-friction shapes may help

External API accepting 2 historical payload versions

Validate both versions at the boundary in either discipline

Repository size, input trust and change frequency determine which feedback loop is useful. For each scenario, ask whether the next signal arrives during checking, boundary validation or execution.

Static vs dynamic typing: the short version and next step

Both baselines produced 1497. Static checking found stale uses before execution; dynamic checking found one on its path. Neither rejected -3 or wrong currency without encoded rules.

Recreate both OrderLine versions. Change 499 to Money("INR", 499), predict every failure, then implement the four-row regression table. Continue structured C, C++, Java and Python practice with Coding For Placements.