Skip to content

Mental Model — Thinking in Python

In TypeScript, the compiler (tsc) is your first line of defence. It catches type mismatches before any code runs. When you move to Python, that safety net disappears at runtime — the interpreter does not check types. Optional type hints exist (and you should use them), but enforcement requires a separate tool like mypy or pyright.

Mental model: TypeScript says “prove it is safe before running.” Python says “run it, and if you pass the wrong type, you will get a runtime error.”

Python variables are labels that can point to any object. The type of the value lives on the object itself, not on the variable name. This is fundamentally different from TypeScript where the variable has a type at compile time.

TypeScript
// TypeScript: variable has a compile-time type
let count: number = 0;
count = "five"; // Error: Type 'string' is not assignable to type 'number'
function double(n: number): number {
return n * 2;
}
Python
# Python: variable is just a label
count = 0
count = "five" # perfectly legal at runtime
def double(n: int) -> int: # hints are documentation only
return n * 2
print(double(3)) # 6
print(double("x")) # TypeError at RUNTIME, not before

TypeScript compiles to JavaScript before running. Python is interpreted — you run source files directly with python3. There is no build step for development.

TypeScript
# TypeScript workflow
# tsc index.ts → compiles to index.js
# node index.js → runs the compiled output
# ts-node index.ts → shortcut for development
Python
# Python workflow
# python3 script.py → runs directly, no compile step
# python3 → opens the interactive REPL
# python3 -c "print(1)" → run a one-liner inline

Significant indentation — no curly braces

Section titled “Significant indentation — no curly braces”

This is the change TypeScript developers notice first. Python uses indentation (4 spaces by convention) to delimit blocks. There are no { and }. A colon (:) always opens a new block.

TypeScript
// TypeScript
function classify(n: number): string {
if (n > 0) {
return "positive";
} else if (n < 0) {
return "negative";
} else {
return "zero";
}
}
Python
# Python — indentation IS the block
def classify(n: int) -> str:
if n > 0:
return "positive"
elif n < 0: # note: elif, not else if
return "negative"
else:
return "zero"
print(classify(5)) # positive
print(classify(-3)) # negative
print(classify(0)) # zero

A misindented line is a syntax error. Most Python editors enforce 4-space indentation automatically.

TypeScript is flexible: you can use classes, functional composition, prototype chains, or plain objects interchangeably. Python encourages a single idiomatic style. The community has a name for it: Pythonic. Pythonic code is concise, readable, and uses language features as intended.

TypeScript
// TypeScript: multiple ways to filter
const evens1 = nums.filter(n => n % 2 === 0);
const evens2 = nums.reduce((acc, n) => n % 2 === 0 ? [...acc, n] : acc, []);
// Both work; TS community accepts both
Python
# Python: list comprehension is the Pythonic way
nums = [1, 2, 3, 4, 5, 6]
evens = [n for n in nums if n % 2 == 0]
print(evens) # [2, 4, 6]
# Works but less Pythonic:
# evens = list(filter(lambda n: n % 2 == 0, nums))

TypeScript class methods implicitly receive this. Python class methods require an explicit self parameter as the first argument — the interpreter fills it in when you call the method, but you must declare it.

TypeScript
// TypeScript: implicit this
class Counter {
private count: number = 0;
increment(): void {
this.count++; // 'this' is implicit
}
value(): number {
return this.count;
}
}
Python
# Python: explicit self
class Counter:
def __init__(self): # called on Counter()
self.count = 0 # instance attribute
def increment(self): # self is the instance
self.count += 1
def value(self) -> int:
return self.count
c = Counter()
c.increment()
c.increment()
print(c.value()) # 2

TypeScript types are erased: tsc deletes them and the JavaScript that runs has no idea they existed. Python keeps its annotations as real objects — which used to mean they were evaluated eagerly, at import time, so a type that did not exist yet had to be quoted as a string.

Python 3.14 (PEP 649) changed that: annotations are stored as a lazily evaluated function and only computed when something actually asks for them.

TypeScript
// TypeScript — types are erased before runtime
interface Node { parent: Node | null; } // self-reference is free
function walk(n: Node): void { /* ... */ }
// Compiled JS has no trace of Node at all
Python
# Python 3.14 — annotations are evaluated on demand
class Node:
parent: Node | None # no quotes needed — not evaluated yet
def add(self, child: Node) -> Node: # forward reference just works
return child
# Read them when you actually want them:
from annotationlib import get_annotations
print(get_annotations(Node.add))

Two practical consequences: you can drop the quotes around forward references, and from __future__ import annotations is no longer something you add to every file. Libraries that inspect annotations at runtime — Pydantic, FastAPI, dataclasses — read them through annotationlib instead of poking at __annotations__ directly.

# Mental model: types live on objects, not variables
x = 42
print(type(x).__name__) # int
x = "hello"
print(type(x).__name__) # str
# Significant indentation
def classify(n: int) -> str:
if n > 0:
return "positive"
elif n < 0:
return "negative"
else:
return "zero"
for num in [5, -3, 0]:
print(f"{num} is {classify(num)}")
# Pythonic list comprehension
evens = [n for n in range(1, 11) if n % 2 == 0]
print("Evens:", evens)
# Explicit self
class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
def value(self) -> int:
return self.count
c = Counter()
c.increment()
c.increment()
print("Counter:", c.value())
In Python, where is the type of a value stored?
What opens a new indented block in Python?
Why must Python class methods declare `self` as their first parameter?
What is the Pythonic way to filter even numbers from a list?